Какие типы переходов в С++ вы используете?

Общеизвестно, что встроенные перечисления в С++ не являются типичными. Мне было интересно, какие классы, реализующие typesafe enums, используются там... Я сам использую следующий "велосипед", но он несколько подробный и ограниченный:

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: Преобразование значения enum в/из int должно быть возможным с помощью одного явного вызова функции/метода.

Приоритет 3: как можно более компактное, элегантное и удобное объявление и использование

Приоритет 4: Преобразование значений перечисления в строки и из строк.

Приоритет 5: (Приятно иметь) Возможность итерации над значениями перечисления.

Ответ 1

Я сейчас играю с предложением 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();
        }
    }
};

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

Ответ 2

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

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

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

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

Ответ 3

Я использую С++ 0x typesafe enums. Я использую некоторые вспомогательные шаблоны/макросы, которые предоставляют функциональные возможности /from.

enum class Result { Ok, Cancel};

Ответ 4

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

Ответ 5

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

Ответ 6

Я лично использую адаптированную версию typafe enum idiom. Он не предоставляет все пять "требований", которые вы указали в своем редактировании, но я сильно не согласен с некоторыми из них. Например, я не вижу, как Prio # 4 (преобразование значений в строки) имеет какое-либо отношение к безопасности типов. В большинстве случаев представление отдельных значений отдельных строк должно быть отдельно от определения типа (подумайте i18n по простой причине). Prio # 5 (iteratio, который является дополнительным) является одной из самых приятных вещей, которые я хотел бы видеть, естественно, происходящих в перечислениях, поэтому мне было грустно, что она выглядит "необязательной" в вашем запросе, но, похоже, ее лучше адресовать через a итерационная система, например, функции begin/end или enum_iterator, что позволяет им работать без проблем с STL и С++ 11 foreach.

OTOH, эта простая идиома прекрасно обеспечивает Prio # 3 Prio # 1 благодаря тому, что она в основном только обертывает enum с большей информацией о типе. Не говоря уже о том, что это очень простое решение, которое по большей части не требует внешних заголовков зависимостей, поэтому его довольно легко переносить. Это также имеет преимущество, заключающееся в том, что перечисления имеют область действия a-la-С++ 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, уже принимающий участие в операторах преобразования, объявленных в другом месте, я бы с радостью получил комментарий об этом).

Объединяя это с вышеприведенной типичной идиомой, вы получаете что-то, что относительно близко к С++ 11 enum class в градации (читаемость и ремонтопригодность) без необходимости делать что-то слишком неясное. И я должен признать, что это было весело, я никогда не думал, чтобы спросить у компилятора, имел ли я дело с enum или нет...

Ответ 7

Я думаю, что 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 являются неизменяемыми, singleton экземплярами Result (с помощью методов, которые вы видите). Вы не можете создавать дополнительные экземпляры Result. Поскольку они являются одиночными, вы можете сравнить по указателю/ссылке --- очень удобно.: -)

ETA: в Java вместо того, чтобы делать битмаски вручную, вместо этого вы используете EnumSet для указания битового набора (он реализует интерфейс Set и работает как наборы --- но реализуется с использованием битмаски). Гораздо более читабельна, чем ручная манипуляция масками!

Ответ 8

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

Недостаток моего подхода заключается в том, что он не программно обеспечивает связь между enum и вспомогательным классом, поэтому их необходимо обновлять параллельно. Это работает для меня, но YMMV.

Ответ 9

В настоящее время я пишу свою собственную библиотеку enum enum в https://bitbucket.org/chopsii/typesafe-enums

Я не самый опытный разработчик С++, но я пишу это из-за недостатков перечислений хранилища BOOST.

Не стесняйтесь проверять и использовать их самостоятельно, но у них есть некоторые (надеюсь, незначительные) проблемы использования и, вероятно, совсем не кросс-платформенные.

Пожалуйста, внесите свой вклад, если хотите. Это мое первое начинание с открытым исходным кодом.

Ответ 10

Используйте 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);
}

Возможно, вы можете создать макрос для создания шаблона. (Дайте мне знать, если вы это сделаете.)

В отличие от других подходов это действительно безопасно и работает со старым С++. Например, вы можете создавать классные типы, например boost::variant<int, A_t, B_t, boost::none>, чтобы представлять значение, которое может быть A, B, целым числом или ничем, что является почти уровнем безопасности типа Haskell98.

Недостатки, о которых нужно знать:

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

Update

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

Ответ 11

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

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