пространства имен для типов перечислений - лучшие практики

104

Часто требуется несколько перечислимых типов вместе. Иногда возникает конфликт имен. На ум приходят два решения: использовать пространство имен или использовать «более крупные» имена элементов перечисления. Тем не менее, решение с пространством имен имеет две возможные реализации: фиктивный класс с вложенным перечислением или полноценное пространство имен.

Я ищу плюсы и минусы всех трех подходов.

Пример:

// oft seen hand-crafted name clash solution
enum eColors { cRed, cColorBlue, cGreen, cYellow, cColorsEnd };
enum eFeelings { cAngry, cFeelingBlue, cHappy, cFeelingsEnd };
void setPenColor( const eColors c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cColorBlue: //...
        //...
    }
 }


// (ab)using a class as a namespace
class Colors { enum e { cRed, cBlue, cGreen, cYellow, cEnd }; };
class Feelings { enum e { cAngry, cBlue, cHappy, cEnd }; };
void setPenColor( const Colors::e c ) {
    switch (c) {
        default: assert(false);
        break; case Colors::cRed: //...
        break; case Colors::cBlue: //...
        //...
    }
 }


 // a real namespace?
 namespace Colors { enum e { cRed, cBlue, cGreen, cYellow, cEnd }; };
 namespace Feelings { enum e { cAngry, cBlue, cHappy, cEnd }; };
 void setPenColor( const Colors::e c ) {
    switch (c) {
        default: assert(false);
        break; case Colors::cRed: //...
        break; case Colors::cBlue: //...
        //...
    }
  }
xtofl
источник
19
Прежде всего, я бы использовал Color :: Red, Feeling: Angry и т. Д.
abatishchev
хороший вопрос, я использовал метод пространства имен ....;)
MiniScalope
18
префикс "c" на всем вредит читабельности.
Пользователь
8
@User: почему, спасибо, что открыли венгерское обсуждение :)
xtofl
5
Обратите внимание, что вам не нужно называть перечисление, как в enum e {...}, перечисления могут быть анонимными, т. Е. Что enum {...}имеет гораздо больший смысл, если оно заключено в пространство имен или класс.
kralyk 06

Ответы:

73

Исходный ответ C ++ 03:

Выгода от А namespace(больше class), что вы можете использовать usingобъявление , когда вы хотите.

Проблема с использованием в namespaceтом , что пространство имен может быть расширен в других местах в коде. В большом проекте вам не будет гарантировано, что два разных перечисления не оба думают, что они вызываютсяeFeelings

Для более простого кода я использую a struct, поскольку вы, вероятно, хотите, чтобы его содержимое было общедоступным.

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

Новее, совет C ++ 11:

Если вы используете C ++ 11 или новее, enum classбудет неявно ограничивать значения перечисления в пределах имени перечисления.

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

Дрю Дорманн
источник
4
Я согласен со структурой идеей. И спасибо за комплимент :)
xtofl
3
+1 Я не мог вспомнить синтаксис C ++ 11 "enum class". Без этой функции перечисления неполны.
Grault
Есть ли у них возможность использовать "using" для неявной области "enum class". например, добавит 'using Color :: e;' чтобы код разрешил использование cRed и знал, что это должно быть Color :: e :: cRed?
JVApen
20

FYI В C ++ 0x есть новый синтаксис для случаев, подобных тому, что вы упомянули (см. Страницу вики C ++ 0x )

enum class eColors { ... };
enum class eFeelings { ... };
Jmihalicza
источник
11

Я гибридизировал предыдущие ответы на что-то вроде этого: (EDIT: это полезно только для pre-C ++ 11. Если вы используете C ++ 11, используйте enum class)

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

Он structизбегает общедоступного синтаксического сахара и typedefпозволяет вам фактически объявлять переменные этих перечислений в других рабочих классах.

Я не думаю, что использование пространства имен вообще помогает. Возможно, это потому, что я программист на C #, и вам нужно использовать имя типа перечисления при ссылке на значения, поэтому я к нему привык.

    struct KeySource {
        typedef enum { 
            None, 
            Efuse, 
            Bbram
        } Type;
    };

    struct Checksum {
        typedef enum {
            None =0,
            MD5 = 1,
            SHA1 = 2,
            SHA2 = 3
        } Type;
    };

    struct Encryption {
        typedef enum {
            Undetermined,
            None,
            AES
        } Type;
    };

    struct File {
        typedef enum {
            Unknown = 0,
            MCS,
            MEM,
            BIN,
            HEX
        } Type;
    };

...

class Worker {
    File::Type fileType;
    void DoIt() {
       switch(fileType) {
       case File::MCS: ... ;
       case File::MEM: ... ;
       case File::HEX: ... ;
    }
}
Марк Лаката
источник
9

Я бы определенно избегал использовать для этого класс; вместо этого используйте пространство имен. Вопрос сводится к тому, использовать ли пространство имен или использовать уникальные идентификаторы для значений перечисления. Лично я бы использовал пространство имен, чтобы мои идентификаторы были короче и, надеюсь, более понятными. Тогда код приложения может использовать директиву using namespace и сделать все более читабельным.

Из вашего примера выше:

using namespace Colors;

void setPenColor( const e c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cBlue: //...
        //...
    }
}
Чарльз Андерсон
источник
Не могли бы вы подсказать, почему вы предпочитаете пространство имен классу?
xtofl
@xtofl: вы не можете написать 'using class Colors'
MSalters
2
@MSalters: вы также не можете писать Colors someColor = Red;, потому что пространство имен не является типом. Colors::e someColor = Red;Вместо этого вам придется писать , что довольно противоречит интуиции.
SasQ
@SasQ Разве вам не пришлось бы использовать Colors::e someColorдаже с a, struct/classесли бы вы хотели использовать его в switchвыражении? Если вы используете анонимный, enumто коммутатор не сможет оценить struct.
Загадка Макбета,
1
Извините, но const e cмне это кажется трудночитаемым :-) Не делайте этого. Однако использование пространства имен - это нормально.
dhaumann
7

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

Возможное преимущество использования класса заключается в том, что они могут использоваться в качестве аргументов типа шаблона, что не относится к пространствам имен:

class Colors {
public:
  enum TYPE {
    Red,
    Green,
    Blue
  };
};

template <typename T> void foo (T t) {
  typedef typename T::TYPE EnumType;
  // ...
}

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

Ричард Корден
источник
Неоткрытие классов также является потенциальным недостатком. Список цветов тоже не ограничен.
MSalters
1
Я думаю, что отказ от повторного выступления в классе - это потенциальное преимущество. Если мне нужно больше цветов, я бы просто перекомпилировал класс с большим количеством цветов. Если я не могу этого сделать (скажем, у меня нет кода), то я ни в коем случае не хочу его трогать.
Томас Эдинг,
@MSalters: Отсутствие возможности повторно открыть класс - не только не недостаток, но и средство безопасности. Потому что, когда можно было бы повторно открыть класс и добавить некоторые значения в перечисление, это могло бы нарушить другой код библиотеки, который уже зависит от этого перечисления и знает только старый набор значений. Затем он с радостью принял бы эти новые значения, но во время выполнения прервался бы, не зная, что с ними делать. Помните принцип открытого-закрытого: класс должен быть закрыт для изменения , но открыт для расширения . Под расширением я подразумеваю не добавление к существующему коду, а обертывание его новым кодом (например, получение).
SasQ
Поэтому, когда вы хотите расширить перечисление, вы должны сделать его новым типом, производным от первого (если бы это было легко возможно в C ++ ...; /). Тогда его можно будет безопасно использовать в новом коде, который понимает эти новые значения, но только старые значения будут приняты (путем их преобразования) старым кодом. Они не должны принимать какие-либо из этих новых значений как имеющие неправильный тип (расширенный). Только старые значения, которые они понимают, принимаются как имеющие правильный (базовый) тип (и случайно также относящиеся к новому типу, поэтому он также может быть принят новым кодом).
SasQ
7

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

#include <cassert>

class Color
{
public:
    typedef enum
    {
        Red,
        Blue,
        Green,
        Yellow
    } enum_type;

private:
    enum_type _val;

public:
    Color(enum_type val = Blue)
        : _val(val)
    {
        assert(val <= Yellow);
    }

    operator enum_type() const
    {
        return _val;
    }
};

void SetPenColor(const Color c)
{
    switch (c)
    {
        case Color::Red:
            // ...
            break;
    }
}

Как показано в приведенном выше примере, с помощью класса вы можете:

  1. запретить (к сожалению, не во время компиляции) C ++ разрешать приведение из недопустимого значения,
  2. установить (ненулевое) значение по умолчанию для вновь созданных перечислений,
  3. добавить дополнительные методы, например, для возврата строкового представления выбора.

Просто обратите внимание, что вам нужно объявить, operator enum_type()чтобы C ++ знал, как преобразовать ваш класс в базовое перечисление. В противном случае вы не сможете передать тип switchвыражению.

Михал Горный
источник
Связано ли это решение как-то с тем, что показано здесь ?: en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Safe_Enum Я думаю о том, как сделать его шаблоном, чтобы мне не приходилось каждый раз переписывать этот шаблон Мне нужно это использовать.
SasQ
@SasQ: похоже, да. Вероятно, это та же идея. Однако я не уверен, что шаблон полезен, если только вы не добавляете туда много «общих» методов.
Michał Górny
1. Не совсем так. Вы можете проверить во время компиляции, что перечисление допустимо, через const_expr или через частный конструктор для int.
xryl669
5

Поскольку перечисления ограничены их охватывающей областью видимости, вероятно, лучше всего обернуть их чем-то, чтобы избежать загрязнения глобального пространства имен и избежать конфликтов имен. Я предпочитаю пространство имен классу просто потому, namespaceчто оно похоже на сумку для хранения, тогда как classощущается как надежный объект (см. Обсуждение structvs. class). Возможное преимущество пространства имен состоит в том, что его можно расширить позже - это полезно, если вы имеете дело со сторонним кодом, который вы не можете изменить.

Это все спорно, конечно, когда мы получаем классы перечислений с C ++ 0x.

Майкл Кристофик
источник
enum classes ... нужно это посмотреть!
xtofl
3

Я также стараюсь объединять свои перечисления в классы.

Как сообщил Ричард Корден, преимущество класса состоит в том, что это тип в смысле C ++, и поэтому вы можете использовать его с шаблонами.

У меня есть специальный класс toolbox :: Enum для моих нужд, который я специализирую для каждого шаблона, который предоставляет базовые функции (в основном: отображение значения enum в std :: string, чтобы ввод-вывод было легче читать).

Мой маленький шаблон также имеет дополнительное преимущество - действительно проверять допустимые значения. Компилятор не очень хорошо проверяет, действительно ли значение находится в перечислении:

typedef enum { False: 0, True: 2 } boolean;
   // The classic enum you don't want to see around your code ;)

int main(int argc, char* argv[])
{
  boolean x = static_cast<boolean>(1);
  return (x == False || x == True) ? 0 : 1;
} // main

Меня всегда беспокоило, что компилятор этого не поймает, поскольку у вас остается значение enum, которое не имеет смысла (и чего вы не ожидаете).

Так же:

typedef enum { Zero: 0, One: 1, Two: 2 } example;

int main(int argc, char* argv[])
{
  example y = static_cast<example>(3);
  return (y == Zero || y == One || y == Two) ? 0 : 1;
} // main

И снова main вернет ошибку.

Проблема в том, что компилятор поместит перечисление в наименьшее доступное представление (здесь нам нужно 2 бита) и что все, что соответствует этому представлению, считается допустимым значением.

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

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

Матье М.
источник
4
Интересный. Вы не возражаете поделиться определением вашего класса Enum?
Momeara