Общеизвестно, что встроенные перечисления в C++ небезопасны. Мне было интересно, какие классы, реализующие типизированные перечисления, используются там ... Я сам использую следующий «велосипед», но он несколько многословен и ограничен:
typesafeenum.h:
struct TypesafeEnum
{
// Construction:
public:
TypesafeEnum(): id (next_id++), name("") {}
TypesafeEnum(const std::string& n): id(next_id++), name(n) {}
// Operations:
public:
bool operator == (const TypesafeEnum& right) const;
bool operator != (const TypesafeEnum& right) const;
bool operator < (const TypesafeEnum& right) const;
std::string to_string() const { return name; }
// Implementation:
private:
static int next_id;
int id;
std::string name;
};
typesafeenum.cpp:
int TypesafeEnum::next_id = 1;
bool TypesafeEnum::operator== (const TypesafeEnum& right) const
{ return id == right.id; }
bool TypesafeEnum::operator!= (const TypesafeEnum& right) const
{ return !operator== (right); }
bool TypesafeEnum::operator< (const TypesafeEnum& right) const
{ return id < right.id; }
Использование:
class Dialog
{
...
struct Result: public TypesafeEnum
{
static const Result CANCEL("Cancel");
static const Result OK("Ok");
};
Result doModal();
...
};
const Dialog::Result Dialog::Result::OK;
const Dialog::Result Dialog::Result::CANCEL;
Добавление: Думаю, мне следовало более конкретно рассказать о требованиях. Попробую их резюмировать:
Приоритет 1: установка для переменной перечисления недопустимого значения должна быть невозможной (ошибка времени компиляции) без исключений.
Приоритет 2: преобразование значения перечисления в / из int должно быть возможным с помощью одного явного вызова функции / метода.
Приоритет 3: Максимально компактное, элегантное и удобное объявление и использование
Приоритет 4: преобразование значений перечисления в строки и из строк.
Приоритет 5: (Приятно иметь) Возможность перебирать значения перечисления.
@Stiver: Извините за долгую задержку, вероятно, больше не актуально, но я все равно отвечу: это была моя ошибка, в исходной версии, в которой у меня не было строк, я прикрутил их перед публикацией без проверки, извините. Правильная версия будет передавать строки конструкторам значений перечисления в файле cpp.
в настоящее время это лучший вопрос по перечислениям и строкам в SO sofar.
Я изменил теги на C++ 03, поскольку явно типизированные перечисления теперь являются частью C++. Обратите внимание, что перечисления уже являются "типизированными" в C++ 2003.


Я не. Слишком много накладных расходов без особой пользы. Кроме того, очень удобным инструментом является возможность преобразовывать перечисления в разные типы данных для сериализации. Я никогда не видел случая, когда перечисление «Типобезопасное» стоило бы накладных расходов и сложности, когда C++ уже предлагает достаточно хорошую реализацию.
Не стреляйте в посыльного ... Голосование "за" снова
Типобезопасное перечисление может быть реализовано с использованием шаблона, так что никаких дополнительных затрат времени выполнения не возникает. И если вы используете, скажем, BOOST_ENUM, вся сложность заключается в библиотеке.
Я думаю, что Java enum будет хорошей моделью для подражания. По сути, форма Java будет выглядеть так:
public enum Result {
OK("OK"), CANCEL("Cancel");
private final String name;
Result(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Что интересно в подходе Java, так это то, что OK и CANCEL являются неизменяемыми одноэлементными экземплярами Result (с методами, которые вы видите). Вы не можете создавать другие экземпляры Result. Поскольку они синглтоны, вы можете сравнивать их по указателю / ссылке --- очень удобно. :-)
ETA: В Java вместо того, чтобы создавать битовые маски вручную, вместо этого вы используете EnumSet для указания набора бит (он реализует интерфейс Set и работает как наборы, но реализован с использованием битовых масок). Намного более читабельный, чем рукописные манипуляции с битовой маской!
Это очень хорошо, но как это сделать в C++? И я думаю, что использование адресов для сравнения не очень безобидно, подумайте, если значения перечисления сериализуются на диске? Каждый раз мы могли получать разные заказы.
Помните, что в Java вы не можете сравнивать < указателей, только == или !=. Итак, пока они различны, этого достаточно для сравнения по указателю. :-) Я посмотрю, смогу ли я когда-нибудь написать реализацию на C++.
java enum кажется экземпляром любопытно повторяющегося шаблона cope (en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern) . naict в гугле, никто не пробовал этот подход. Интересно, связано ли это с разницей между дженериками java и семантикой stl?
Хороший компромиссный метод заключается в следующем:
struct Flintstones {
enum E {
Fred,
Barney,
Wilma
};
};
Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;
Это не типобезопасно в том же смысле, что и ваша версия, но использование лучше, чем стандартные перечисления, и вы все равно можете воспользоваться преимуществом целочисленного преобразования, когда оно вам нужно.
Разве это не было бы чище с пространством имен вместо структуры?
Интересная мысль. Это устранило бы одну случайную проблему с этим, когда кто-то пытается объявить Flintstones, а не Flintstones :: E.
Продолжение вопроса о пространстве имен: для перечислений, которые находятся внутри класса или структуры, вы не можете использовать пространство имен. Если перечисление не находится в классе или структуре, я думаю, вы правы в том, что использование пространства имен чище.
Другое решение - предоставить структуре частный конструктор, и он будет работать во вложенном случае.
классы перечисления C++ 11 делают это полностью избыточным
Я дал ответ на этот здесь, по другой теме. Это другой стиль подхода, который позволяет использовать большую часть той же функциональности, не требуя изменения исходного определения перечисления (и, следовательно, разрешая использование в тех случаях, когда вы не определяете перечисление). Он также позволяет проверять диапазон во время выполнения.
Обратной стороной моего подхода является то, что он не обеспечивает программного обеспечения связи между перечислением и вспомогательным классом, поэтому их нужно обновлять параллельно. У меня работает, но YMMV.
Я считаю, что вы придумываете проблему, а затем подбираете для нее решение. Я не вижу необходимости создавать сложную структуру для перечисления ценностей. Если вы преданный, чтобы ваши значения были только членами определенного набора, вы можете взломать вариант уникального типа данных набора.
В настоящее время я играю с предложением Boost.Enum из Boost Vault (имя файла enum_rev4.6.zip). Хотя он никогда официально не подавался на включение в Boost, его можно использовать как есть. (Документация отсутствует, но она компенсируется ясным исходным кодом и хорошими тестами.)
Boost.Enum позволяет вам объявить перечисление следующим образом:
BOOST_ENUM_VALUES(Level, const char*,
(Abort)("unrecoverable problem")
(Error)("recoverable problem")
(Alert)("unexpected behavior")
(Info) ("expected behavior")
(Trace)("normal flow of execution")
(Debug)("detailed object state listings")
)
И пусть он автоматически расширяется до этого:
class Level : public boost::detail::enum_base<Level, string>
{
public:
enum domain
{
Abort,
Error,
Alert,
Info,
Trace,
Debug,
};
BOOST_STATIC_CONSTANT(index_type, size = 6);
Level() {}
Level(domain index) : boost::detail::enum_base<Level, string>(index) {}
typedef boost::optional<Level> optional;
static optional get_by_name(const char* str)
{
if (strcmp(str, "Abort") == 0) return optional(Abort);
if (strcmp(str, "Error") == 0) return optional(Error);
if (strcmp(str, "Alert") == 0) return optional(Alert);
if (strcmp(str, "Info") == 0) return optional(Info);
if (strcmp(str, "Trace") == 0) return optional(Trace);
if (strcmp(str, "Debug") == 0) return optional(Debug);
return optional();
}
private:
friend class boost::detail::enum_base<Level, string>;
static const char* names(domain index)
{
switch(index)
{
case Abort: return "Abort";
case Error: return "Error";
case Alert: return "Alert";
case Info: return "Info";
case Trace: return "Trace";
case Debug: return "Debug";
default: return NULL;
}
}
typedef boost::optional<value_type> optional_value;
static optional_value values(domain index)
{
switch(index)
{
case Abort: return optional_value("unrecoverable problem");
case Error: return optional_value("recoverable problem");
case Alert: return optional_value("unexpected behavior");
case Info: return optional_value("expected behavior");
case Trace: return optional_value("normal flow of execution");
case Debug: return optional_value("detailed object state listings");
default: return optional_value();
}
}
};
Он удовлетворяет всем пяти перечисленным вами приоритетам.
зачем это нужно для решения проблемы, которой не существует ?? !! голова взрывается
Нет необходимости во взрывающихся головах; сложность красиво скрыта за макросами и чистым интерфейсом. А у стандартных перечислений C есть несколько реальных проблем: отсутствие безопасности типов, отсутствие возможности запрашивать у типа перечисления его максимальный размер, отсутствие автоматического преобразования в / из строк.
Первоначально я проектировал boost :: enum как своего рода таблицу строк, которая была построена во время компиляции и могла использоваться во время выполнения. В конце концов он превратился в более универсальное типобезопасное перечисление. Его так и не включили в Boost из-за нехватки времени на документацию. Приятно видеть это в дикой природе!
Это выглядит очень полезным, вы все равно должны попытаться ввести его. Может быть, версия, которая использует новые строго типизированные перечисления C++ 0x (все же приятно получить интерфейс boost :: optional и преобразование строк).
Да, во-вторых, я только что загрузил и начал успешно пользоваться менее чем за час, очень полезно :)
BOOST_ENUM в версии 4.6 не полностью типобезопасен, особенно когда дело доходит до operator == (). Например. BOOST_ENUM (Apple, (а)); BOOST_ENUM (оранжевый, (o)); bool x = Apple (Apple :: a) == Orange :: o; компилируется нормально. Причина в том, что ctor enum_base (index_type index) не является явным, а index_type равно int. Не уверен насчет других операторов.
Я использую Типизированные перечисления C++ 0x. Я использую несколько вспомогательных шаблонов / макросов, которые обеспечивают функциональность строки в / из.
enum class Result { Ok, Cancel};
Какие компиляторы это поддерживают?
Почему бы не спросить stackoverflow? :-) stackoverflow.com/questions/934183/…
@Roddy: в основном вы используете функцию, которую поддерживает только компилятор один, в современных системах (в отличие от большинства систем, в которых еще нет gcc 4.4)?
@Konrad: Нет, я использую тот, который поддерживают как минимум два компилятора (и я не использую GCC 4.4). Переносимость кода для других компиляторов не является проблемой для меня в моих текущих проектах.
Я почти уверен, что последняя версия MSVC тоже будет его поддерживать. Если он работает в Linux (GCC), OS X (GCC) и Windows (MSVC), это «достаточно хорошая» переносимость для значительного числа пользователей. Конечно, не для всех;)
@Joseph: Эмм, какая версия Visual Studio поддерживает это? 2010, конечно же, нет, по крайней мере, для неуправляемого кода. И это означает нечто смутно иное в мире управляемого C++ / CLI.
@Cody: Похоже, ты прав: stackoverflow.com/questions/2603314/…
@Cody: Вы правы, это просто потому, что MSVC 2010 включает поддержку множества других конструкций C++ 0x, которые, как я думал, уже реализовали все низко висящие плоды.
@ Джозеф: Достаточно честно. Я ожидал того же, но, к сожалению, этого не произошло. Итак, почему я просматривал ответы на этот вопрос. :-) Может в следующей версии ...
@CodyGray Поддерживается с MSVC 2012;)
@ Этьен. К сожалению, цветные и строчные буквы больше не поддерживаются.
Не уверен, что этот пост слишком поздно, но на GameDev.net есть статья, которая удовлетворяет всем, кроме 5-го пункта (возможность перебирать счетчики): http://www.gamedev.net/reference/snippets/features/cppstringizing/
Описанный в статье метод позволяет поддерживать преобразование строк для существующие перечни без изменения их кода. Если вам нужна только поддержка новых перечислений, я бы выбрал Boost.Enum (упомянутый выше).
Думаю, это новая ссылка: gamedev.net/page/resources/_/technical/general-programming/…
Я лично использую адаптированную версию typeafe enum идиома. Он не обеспечивает всех пяти «требований», которые вы указали в своей редакции, но я все равно категорически не согласен с некоторыми из них. Например, я не понимаю, как Prio # 4 (преобразование значений в строки) имеет какое-либо отношение к безопасности типов. В большинстве случаев строковое представление отдельных значений в любом случае должно быть отделено от определения типа (подумайте о i18n по простой причине). Prio # 5 (iteratio, необязательный) - одна из самых приятных вещей, которые я бы хотел видеть в перечислениях, поэтому мне было грустно, что в вашем запросе он отображается как «необязательный», но, похоже, его лучше решить через отдельная итерационная система, такой как функции begin / end, или enum_iterator, что позволяет им без проблем работать с STL и C++ 11 foreach.
OTOH эта простая идиома прекрасно обеспечивает Prio # 3 Prio # 1 благодаря тому, что в основном она только обертывает enum с дополнительной информацией о типе. Не говоря уже о том, что это очень простое решение, которое по большей части не требует никаких внешних заголовков зависимостей, поэтому его довольно легко носить с собой. Он также имеет то преимущество, что перечисления имеют область видимости а-ля-C++ 11:
// This doesn't compile, and if it did it wouldn't work anyway
enum colors { salmon, .... };
enum fishes { salmon, .... };
// This, however, works seamlessly.
struct colors_def { enum type { salmon, .... }; };
struct fishes_def { enum type { salmon, .... }; };
typedef typesafe_enum<colors_def> colors;
typedef typesafe_enum<fishes_def> fishes;
Единственная «дыра», которую предоставляет решение, заключается в том, что оно не учитывает тот факт, что оно не препятствует прямому сравнению enum разных типов (или enum и int), потому что, когда вы используете значения напрямую, вы заставляете неявное преобразование в int:
if (colors::salmon == fishes::salmon) { .../* Ooops! */... }
Но до сих пор я обнаружил, что такие проблемы можно решить, просто предлагая лучшее сравнение с компилятором - например, явно предоставляя оператор, который сравнивает любые два разных типа enum, а затем заставляя его терпеть неудачу:
// I'm using backports of C++11 utilities like static_assert and enable_if
template <typename Enum1, typename Enum2>
typename enable_if< (is_enum<Enum1>::value && is_enum<Enum2>::value) && (false == is_same<Enum1,Enum2>::value) , bool >
::type operator== (Enum1, Enum2) {
static_assert (false, "Comparing enumerations of different types!");
}
Хотя до сих пор это, похоже, не нарушает код и явно решает конкретную проблему, не делая чего-то еще, я не уверен, что это именно то, что делает один "должен" (я подозреваю, что это будет мешать enum уже участвуют в заявленных где-то конвертирующих операторах, я бы с удовольствием получил комментарий по этому поводу).
Сочетание этого с приведенной выше идиомой типизации дает нечто, относительно близкое к C++ 11 enum class по понятности (удобочитаемости и ремонтопригодности) без необходимости делать что-то слишком непонятное. И я должен признать, что это было весело, я никогда не думал, что на самом деле спросить компилятор, имел ли я дело с enum или нет ...
В вашем первом примере кода вы, кажется, используете класс шаблона typesafe_enum - есть ли шанс показать код для этого? В противном случае трудно понять ответ (если я не упускаю что-то очевидное).
В настоящее время я пишу свою собственную библиотеку типов безопасного перечисления в https://bitbucket.org/chopsii/typesafe-enums
Я не самый опытный разработчик C++, но пишу это из-за недостатков перечислений хранилища BOOST.
Не стесняйтесь проверить это и использовать их самостоятельно, но у них есть некоторые (надеюсь, незначительные) проблемы с удобством использования, и, вероятно, они совсем не кроссплатформенные.
Пожалуйста, внесите свой вклад, если хотите. Это мой первый проект с открытым исходным кодом.
Используйте boost::variant!
Попробовав многие из вышеперечисленных идей и обнаружив, что их не хватает, я нашел такой простой подход:
#include <iostream>
#include <boost/variant.hpp>
struct A_t {};
static const A_t A = A_t();
template <typename T>
bool isA(const T & x) { if (boost::get<A_t>(&x)) return true; return false; }
struct B_t {};
static const B_t B = B_t();
template <typename T>
bool isB(const T & x) { if (boost::get<B_t>(&x)) return true; return false; }
struct C_t {};
static const C_t C = C_t();
template <typename T>
bool isC(const T & x) { if (boost::get<C_t>(&x)) return true; return false; }
typedef boost::variant<A_t, B_t> AB;
typedef boost::variant<B_t, C_t> BC;
void ab(const AB & e)
{
if (isA(e))
std::cerr << "A!" << std::endl;
if (isB(e))
std::cerr << "B!" << std::endl;
// ERROR:
// if (isC(e))
// std::cerr << "C!" << std::endl;
// ERROR:
// if (e == 0)
// std::cerr << "B!" << std::endl;
}
void bc(const BC & e)
{
// ERROR:
// if (isA(e))
// std::cerr << "A!" << std::endl;
if (isB(e))
std::cerr << "B!" << std::endl;
if (isC(e))
std::cerr << "C!" << std::endl;
}
int main() {
AB a;
a = A;
AB b;
b = B;
ab(a);
ab(b);
ab(A);
ab(B);
// ab(C); // ERROR
// bc(A); // ERROR
bc(B);
bc(C);
}
Вероятно, вы можете придумать макрос для создания шаблона. (Дайте мне знать, если вы это сделаете.)
В отличие от других подходов, этот на самом деле типобезопасен и работает со старым C++. Вы даже можете создать классные типы, такие как boost::variant<int, A_t, B_t, boost::none>, например, для представления значения, которое может быть A, B, целым числом или ничем, что почти соответствует уровням безопасности типов Haskell98.
Следует помнить о недостатках:
Здесь для вашего удобства находится ваша "библиотека" typeafe-enum. Вставьте этот заголовок:
#ifndef _TYPESAFE_ENUMS_H
#define _TYPESAFE_ENUMS_H
#include <string>
#include <boost/variant.hpp>
#define ITEM(NAME, VAL) \
struct NAME##_t { \
std::string toStr() const { return std::string( #NAME ); } \
int toInt() const { return VAL; } \
}; \
static const NAME##_t NAME = NAME##_t(); \
template <typename T> \
bool is##NAME(const T & x) { if (boost::get<NAME##_t>(&x)) return true; return false; } \
class toStr_visitor: public boost::static_visitor<std::string> {
public:
template<typename T>
std::string operator()(const T & a) const {
return a.toStr();
}
};
template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
std::string toStr(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
return boost::apply_visitor(toStr_visitor(), a);
}
class toInt_visitor: public boost::static_visitor<int> {
public:
template<typename T>
int operator()(const T & a) const {
return a.toInt();
}
};
template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
int toInt(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
return boost::apply_visitor(toInt_visitor(), a);
}
#define ENUM(...) \
typedef boost::variant<__VA_ARGS__>
#endif
И используйте это так:
ITEM(A, 0);
ITEM(B, 1);
ITEM(C, 2);
ENUM(A_t, B_t) AB;
ENUM(B_t, C_t) BC;
Обратите внимание, что вы должны сказать A_t вместо A в макросе ENUM, что разрушает часть волшебства. Ну что ж. Также обратите внимание, что теперь есть функция toStr и функция toInt для удовлетворения требований OP о простом преобразовании в строки и целые числа. Требование, которое я не могу понять, - это способ перебора элементов. Дайте мне знать, если вы умеете писать такие вещи.
Извините, какой компилятор вы используете? MSVC 2008 не может скомпилировать этот пример. Это - статическая константа Result CANCEL («Отмена»); - не похоже на действительный код C++ ...