Вопрос

Общеизвестно, что встроенные перечисления в 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:Присвоение переменной enum недопустимого значения должно быть невозможным (ошибка времени компиляции) без каких-либо исключений.

Приоритет 2:Преобразование значения enum в / из int должно быть возможно с помощью одного явного вызова функции / метода.

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

Приоритет 4:Преобразование значений enum в строки и из них.

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

Это было полезно?

Решение

В настоящее время я играю с повышением.Предложение по перечислению от Увеличьте Хранилище (имя файла 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();
        }
    }
};

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

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

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

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

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

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

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

enum class Result { Ok, Cancel};

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

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

Лично я использую адаптированную версию типобезопасная идиома перечисления.Это не обеспечивает всех пяти "требований", которые вы указали в своей правке, но я все равно категорически не согласен с некоторыми из них.Например, я не вижу, как Prio # 4 (преобразование значений в строки) имеет какое-либо отношение к безопасности типов.Большую часть времени строковое представление отдельных значений в любом случае должно быть отделено от определения типа (думаю, i18n по простой причине, почему).Prio # 5 (итерация, которая необязательна) - одна из самых приятных вещей, которые я хотел бы видеть естественно происходит в перечислениях, поэтому мне стало грустно, что в вашем запросе это указано как "необязательное", но, похоже, это лучше решать через отдельная итерационная система такие , как begin/end функции или enum_iterator, что позволяет им беспрепятственно работать с STL и C ++ 11 foreach.

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

Единственная "дыра", которую создает это решение, заключается в том, что оно не учитывает тот факт, что оно не предотвращает enums разных типов (или 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ы уже принимают участие в операторах преобразования, объявленных в другом месте;Я бы с удовольствием получил комментарий по этому поводу).

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

Я думаю, что 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 интерфейс, и работает как наборы ---но реализован с использованием битовых масок).Гораздо более читабельна, чем рукописные манипуляции с битовой маской!

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

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

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

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

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

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

Использование 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, целым числом или ничем, что соответствует почти 98 уровням безопасности типов Haskell.

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

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

Обновить

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top