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

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

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

Пример:

// 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: //...
        //...
    }
  }

Ответ 1

Оригинальный ответ С++ 03:

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

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

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

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

Более новый, совет С++ 11:

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

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

Ответ 2

FYI В С++ 0x существует новый синтаксис для таких случаев, как то, что вы упомянули (см. С++ 0x wiki page)

enum class eColors { ... };
enum class eFeelings { ... };

Ответ 3

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

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

using namespace Colors;

void setPenColor( const e c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cBlue: //...
        //...
    }
}

Ответ 4

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

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

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

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

    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: ... ;
    }
}

Ответ 5

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

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

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

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

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

Ответ 6

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

#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;
    }
}

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

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

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

Ответ 7

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

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

Ответ 8

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

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

У меня есть специальный класс 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

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

Аналогично:

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 бита) и что все, что соответствует этому представлению, считается допустимым значением.

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

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