Какой тип Enum в C ++ вы используете? - PullRequest
44 голосов
/ 20 октября 2008

Общеизвестно, что встроенные перечисления в 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. Преобразование значений перечисления в и из строк.

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

Ответы [ 11 ]

42 голосов
/ 13 января 2009

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

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

19 голосов
/ 20 октября 2008

Хороший компромиссный метод таков:

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

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

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

12 голосов
/ 13 января 2009

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

enum class Result { Ok, Cancel};
6 голосов
/ 20 октября 2008

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

2 голосов
/ 08 августа 2012

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

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

Единственная «дыра», которую обеспечивает решение, заключается в том, что оно не учитывает тот факт, что оно не препятствует прямому сравнению enum s различных типов (или 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!");
}

Хотя кажется, что пока он не нарушает код и явно решает конкретную проблему, не занимаясь чем-то другим, я не уверен, что это такая вещь " должна msgstr "делать (я подозреваю, что это будет мешать enum s, уже участвующим в операторах преобразования, объявленных в другом месте; я с удовольствием получу комментарий об этом).

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

2 голосов
/ 20 октября 2008

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

1 голос
/ 20 октября 2008

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

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

1 голос
/ 20 октября 2008

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

0 голосов
/ 06 октября 2016

Использование 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 ++ для вас

Обновление

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

0 голосов
/ 16 января 2015

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...