Какие типы типов Enum в C++ вы используете?

Общеизвестно, что встроенные перечисления в 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: (Приятно иметь) Возможность перебирать значения перечисления.

Извините, какой компилятор вы используете? MSVC 2008 не может скомпилировать этот пример. Это - статическая константа Result CANCEL («Отмена»); - не похоже на действительный код C++ ...

Stiver 09.08.2012 14:23

@Stiver: Извините за долгую задержку, вероятно, больше не актуально, но я все равно отвечу: это была моя ошибка, в исходной версии, в которой у меня не было строк, я прикрутил их перед публикацией без проверки, извините. Правильная версия будет передавать строки конструкторам значений перечисления в файле cpp.

Alex Jenter 13.11.2013 15:04

в настоящее время это лучший вопрос по перечислениям и строкам в SO sofar.

Alexander Oh 15.12.2013 23:56

Я изменил теги на C++ 03, поскольку явно типизированные перечисления теперь являются частью C++. Обратите внимание, что перечисления уже являются "типизированными" в C++ 2003.

Sebastian Mach 17.06.2014 00:39
Повышение качества Laravel с помощью принципов SOLID: Лучшие практики и примеры
Повышение качества Laravel с помощью принципов SOLID: Лучшие практики и примеры
Когда мы говорим о том, как сделать следующий шаг в качестве разработчика, мы должны понимать, что качество кода всегда является основным фокусом на...
Принципы SOLID - лучшие практики
Принципы SOLID - лучшие практики
SOLID - это аббревиатура, обозначающая пять ключевых принципов проектирования: принцип единой ответственности, принцип "открыто-закрыто", принцип...
45
4
25 636
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Я не. Слишком много накладных расходов без особой пользы. Кроме того, очень удобным инструментом является возможность преобразовывать перечисления в разные типы данных для сериализации. Я никогда не видел случая, когда перечисление «Типобезопасное» стоило бы накладных расходов и сложности, когда C++ уже предлагает достаточно хорошую реализацию.

Не стреляйте в посыльного ... Голосование "за" снова

fizzer 20.10.2008 11:13

Типобезопасное перечисление может быть реализовано с использованием шаблона, так что никаких дополнительных затрат времени выполнения не возникает. И если вы используете, скажем, BOOST_ENUM, вся сложность заключается в библиотеке.

Joseph Garvin 14.12.2009 20:33

Я думаю, что 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++? И я думаю, что использование адресов для сравнения не очень безобидно, подумайте, если значения перечисления сериализуются на диске? Каждый раз мы могли получать разные заказы.

Alex Jenter 20.10.2008 09:47

Помните, что в Java вы не можете сравнивать < указателей, только == или !=. Итак, пока они различны, этого достаточно для сравнения по указателю. :-) Я посмотрю, смогу ли я когда-нибудь написать реализацию на C++.

Chris Jester-Young 20.10.2008 09:57

java enum кажется экземпляром любопытно повторяющегося шаблона cope (en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern) ‌. naict в гугле, никто не пробовал этот подход. Интересно, связано ли это с разницей между дженериками java и семантикой stl?

Ray Tayek 20.10.2008 19:39

Хороший компромиссный метод заключается в следующем:

struct Flintstones {
   enum E {
      Fred,
      Barney,
      Wilma
   };
};

Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;

Это не типобезопасно в том же смысле, что и ваша версия, но использование лучше, чем стандартные перечисления, и вы все равно можете воспользоваться преимуществом целочисленного преобразования, когда оно вам нужно.

Разве это не было бы чище с пространством имен вместо структуры?

jmucchiello 07.02.2009 20:07

Интересная мысль. Это устранило бы одну случайную проблему с этим, когда кто-то пытается объявить Flintstones, а не Flintstones :: E.

Charlie 08.02.2009 18:19

Продолжение вопроса о пространстве имен: для перечислений, которые находятся внутри класса или структуры, вы не можете использовать пространство имен. Если перечисление не находится в классе или структуре, я думаю, вы правы в том, что использование пространства имен чище.

Charlie 17.10.2009 06:40

Другое решение - предоставить структуре частный конструктор, и он будет работать во вложенном случае.

Joseph Garvin 26.06.2010 00:09

классы перечисления C++ 11 делают это полностью избыточным

sehe 26.02.2014 17:52

Я дал ответ на этот здесь, по другой теме. Это другой стиль подхода, который позволяет использовать большую часть той же функциональности, не требуя изменения исходного определения перечисления (и, следовательно, разрешая использование в тех случаях, когда вы не определяете перечисление). Он также позволяет проверять диапазон во время выполнения.

Обратной стороной моего подхода является то, что он не обеспечивает программного обеспечения связи между перечислением и вспомогательным классом, поэтому их нужно обновлять параллельно. У меня работает, но 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();
        }
    }
};

Он удовлетворяет всем пяти перечисленным вами приоритетам.

зачем это нужно для решения проблемы, которой не существует ?? !! голова взрывается

user19302 15.01.2009 00:42

Нет необходимости во взрывающихся головах; сложность красиво скрыта за макросами и чистым интерфейсом. А у стандартных перечислений C есть несколько реальных проблем: отсутствие безопасности типов, отсутствие возможности запрашивать у типа перечисления его максимальный размер, отсутствие автоматического преобразования в / из строк.

Josh Kelley 15.01.2009 01:21

Первоначально я проектировал boost :: enum как своего рода таблицу строк, которая была построена во время компиляции и могла использоваться во время выполнения. В конце концов он превратился в более универсальное типобезопасное перечисление. Его так и не включили в Boost из-за нехватки времени на документацию. Приятно видеть это в дикой природе!

fried 22.11.2009 13:21

Это выглядит очень полезным, вы все равно должны попытаться ввести его. Может быть, версия, которая использует новые строго типизированные перечисления C++ 0x (все же приятно получить интерфейс boost :: optional и преобразование строк).

Joseph Garvin 07.01.2010 22:47

Да, во-вторых, я только что загрузил и начал успешно пользоваться менее чем за час, очень полезно :)

radman 28.06.2010 10:15

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. Не уверен насчет других операторов.

Andreas Haferburg 11.01.2013 22:18

Я использую Типизированные перечисления C++ 0x. Я использую несколько вспомогательных шаблонов / макросов, которые обеспечивают функциональность строки в / из.

enum class Result { Ok, Cancel};

Какие компиляторы это поддерживают?

Joachim Sauer 27.10.2009 14:30

Почему бы не спросить stackoverflow? :-) stackoverflow.com/questions/934183/…

Roddy 27.10.2009 15:06

@Roddy: в основном вы используете функцию, которую поддерживает только компилятор один, в современных системах (в отличие от большинства систем, в которых еще нет gcc 4.4)?

Konrad Rudolph 27.11.2009 16:30

@Konrad: Нет, я использую тот, который поддерживают как минимум два компилятора (и я не использую GCC 4.4). Переносимость кода для других компиляторов не является проблемой для меня в моих текущих проектах.

Roddy 08.12.2009 16:15

Я почти уверен, что последняя версия MSVC тоже будет его поддерживать. Если он работает в Linux (GCC), OS X (GCC) и Windows (MSVC), это «достаточно хорошая» переносимость для значительного числа пользователей. Конечно, не для всех;)

Joseph Garvin 14.12.2009 20:34

@Joseph: Эмм, какая версия Visual Studio поддерживает это? 2010, конечно же, нет, по крайней мере, для неуправляемого кода. И это означает нечто смутно иное в мире управляемого C++ / CLI.

Cody Gray 30.06.2011 12:32

@Cody: Похоже, ты прав: stackoverflow.com/questions/2603314/…

Roddy 30.06.2011 12:39

@Cody: Вы правы, это просто потому, что MSVC 2010 включает поддержку множества других конструкций C++ 0x, которые, как я думал, уже реализовали все низко висящие плоды.

Joseph Garvin 30.06.2011 19:05

@ Джозеф: Достаточно честно. Я ожидал того же, но, к сожалению, этого не произошло. Итак, почему я просматривал ответы на этот вопрос. :-) Может в следующей версии ...

Cody Gray 30.06.2011 19:07

@CodyGray Поддерживается с MSVC 2012;)

Étienne 05.06.2014 17:29

@ Этьен. К сожалению, цветные и строчные буквы больше не поддерживаются.

Cody Gray 06.06.2014 10:55

Не уверен, что этот пост слишком поздно, но на GameDev.net есть статья, которая удовлетворяет всем, кроме 5-го пункта (возможность перебирать счетчики): http://www.gamedev.net/reference/snippets/features/cppstringizing/

Описанный в статье метод позволяет поддерживать преобразование строк для существующие перечни без изменения их кода. Если вам нужна только поддержка новых перечислений, я бы выбрал Boost.Enum (упомянутый выше).

Думаю, это новая ссылка: gamedev.net/page/resources/_/technical/general-programming/…

LightStruk 06.02.2013 21:32

Я лично использую адаптированную версию 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 - есть ли шанс показать код для этого? В противном случае трудно понять ответ (если я не упускаю что-то очевидное).

JBentley 28.03.2013 04:04

Возможно, я был недостаточно очевиден. По сути, это тот же код, что и typesafe_enum в Викиучебнике (на который я также ссылался выше). Я добавил к этому пару макросов, чтобы упростить объявления и сделать его совместимым с C++ 11. Возможно, мне стоит его где-нибудь выложить.

Luis Machuca 29.03.2013 08:16

В настоящее время я пишу свою собственную библиотеку типов безопасного перечисления в 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.

Следует помнить о недостатках:

  • по крайней мере, со старым бустом - я использую систему с бустом 1.33 - в вашем варианте вы ограничены 20 предметами; Однако есть обходной путь
  • влияет на время компиляции
  • безумные сообщения об ошибках - но это C++ для вас

Обновлять

Здесь для вашего удобства находится ваша "библиотека" 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 о простом преобразовании в строки и целые числа. Требование, которое я не могу понять, - это способ перебора элементов. Дайте мне знать, если вы умеете писать такие вещи.

Другие вопросы по теме