Почему перечислимый класс предпочтительнее обычного перечисления?

429

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

Но что это на самом деле означает?

Алексей
источник
57
Когда кто-то заявляет, что какая-то программная конструкция является «злой», он пытается отговорить вас думать за себя.
Пит Беккер
3
@NicolBolas: Это скорее риторический вопрос, чтобы дать ответ на часто задаваемые вопросы (действительно ли это часто задаваемый вопрос - другая история).
Дэвид Родригес - dribeas
@ Давид, есть обсуждение, должно ли это быть часто задаваемыми вопросами или нет, которое начинается здесь . Вход приветствуется.
ВОО
17
@PeteBecker Иногда они просто пытаются защитить вас от себя.
piccy
geeksforgeeks.org/… Это также хорошее место для понимания enumпротив enum class.
mr_azad

Ответы:

473

C ++ имеет два вида enum:

  1. enum classэс
  2. Обычный enumс

Вот пара примеров, как их объявить:

 enum class Color { red, green, blue }; // enum class
 enum Animal { dog, cat, bird, human }; // plain enum 

В чем разница между двумя?

  • enum classes - имена перечислителя являются локальными для перечисления, и их значения неявным образом не преобразуются в другие типы (например, другие enumили int)

  • Простые enums - где имена перечислителей находятся в той же области, что и перечисление, и их значения неявно преобразуются в целые и другие типы

Пример:

enum Color { red, green, blue };                    // plain enum 
enum Card { red_card, green_card, yellow_card };    // another plain enum 
enum class Animal { dog, deer, cat, bird, human };  // enum class
enum class Mammal { kangaroo, deer, human };        // another enum class

void fun() {

    // examples of bad use of plain enums:
    Color color = Color::red;
    Card card = Card::green_card;

    int num = color;    // no problem

    if (color == Card::red_card) // no problem (bad)
        cout << "bad" << endl;

    if (card == Color::green)   // no problem (bad)
        cout << "bad" << endl;

    // examples of good use of enum classes (safe)
    Animal a = Animal::deer;
    Mammal m = Mammal::deer;

    int num2 = a;   // error
    if (m == a)         // error (good)
        cout << "bad" << endl;

    if (a == Mammal::deer) // error (good)
        cout << "bad" << endl;

}

Вывод:

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

Алексей
источник
7
Хороший пример ... есть ли способ объединить безопасность типов версии класса с продвижением пространства имен версии enum? То есть, если у меня есть класс Aс состоянием, и я создаю enum class State { online, offline };дочерний класс A, я хотел бы делать state == onlineпроверки внутри Aвместо state == State::online... возможно ли это?
отметка
31
Нет. Раскрутка пространства имен - это плохая вещь, и половиной оправдания enum classбыло ее устранение.
Щенок
10
В C ++ 11 вы также можете использовать явно типизированные перечисления, такие как enum Animal: unsigned int {собака, олень, кошка, птица}
Блазиус Секунд
3
@ Cat Plus Plus Я понимаю, что @ Алексей говорит, что это плохо. Мой вопрос был не в том, думал ли Алексей, что это плохо. На мой вопрос была просьба подробно рассказать, что в этом плохого. В частности, почему Алексей, например, считает плохим Color color = Color::red.
chux - Восстановить Монику
9
@Cat Plus Plus Так что пример плохого не происходит, пока if (color == Card::red_card)строка, на 4 строки позже, чем комментарий (который я сейчас вижу, относится к первой половине блока.) 2 строки блока дают плохие примеры. Первые 3 строки не являются проблемой. «Весь блок - это то, почему простые перечисления плохи», и я подумал, что вы тоже имели в виду, что с ними что-то не так. Теперь я вижу, это просто установка. В любом случае спасибо за отзыв.
chux - Восстановить Монику
248

Из C ++ 11 Bjarne Stroustrup : часто задаваемые вопросы :

В enum classы ( «новые перечисления», «сильные Перечисления») адрес три проблемы с перечислениями традиционными C ++:

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

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

Таким образом, как упоминали другие пользователи, «сильные перечисления» сделали бы код более безопасным.

Базовый тип «классического» enumдолжен быть целочисленным типом, достаточно большим, чтобы соответствовать всем значениям enum; это обычно int. Также каждый перечисляемый тип должен быть совместим с charцелочисленным типом со знаком или без знака.

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

Например, я видел такой код несколько раз:

enum E_MY_FAVOURITE_FRUITS
{
    E_APPLE      = 0x01,
    E_WATERMELON = 0x02,
    E_COCONUT    = 0x04,
    E_STRAWBERRY = 0x08,
    E_CHERRY     = 0x10,
    E_PINEAPPLE  = 0x20,
    E_BANANA     = 0x40,
    E_MANGO      = 0x80,
    E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell?
};

В приведенном выше коде какой-то наивный кодер думает, что компилятор будет хранить E_MY_FAVOURITE_FRUITSзначения в 8-битном типе без знака ... но на это нет никаких гарантий: компилятор может выбрать unsigned charили, intили shortлюбой из этих типов достаточно большой, чтобы вместить все значения видно в enum. Добавление поля E_MY_FAVOURITE_FRUITS_FORCE8является бременем и не заставляет компилятор делать какой-либо выбор в отношении базового типа enum.

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

И что еще хуже, если какой-то напарник небрежно добавляет новое значение к нашему enum:

    E_DEVIL_FRUIT  = 0x100, // New fruit, with value greater than 8bits

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

Так как в C ++ 11 возможно указать базовый тип для enumи enum class(спасибо rdb ), поэтому эта проблема аккуратно решена:

enum class E_MY_FAVOURITE_FRUITS : unsigned char
{
    E_APPLE        = 0x01,
    E_WATERMELON   = 0x02,
    E_COCONUT      = 0x04,
    E_STRAWBERRY   = 0x08,
    E_CHERRY       = 0x10,
    E_PINEAPPLE    = 0x20,
    E_BANANA       = 0x40,
    E_MANGO        = 0x80,
    E_DEVIL_FRUIT  = 0x100, // Warning!: constant value truncated
};

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

Я думаю, что это хорошее улучшение безопасности.

Так почему класс enum предпочтительнее обычного enum? , если мы можем выбрать базовый тип для перечислений scoped ( enum class) и unscoped ( enum), что еще делает enum classлучший выбор?

  • Они не конвертируются неявно в int.
  • Они не загрязняют окружающее пространство имен.
  • Они могут быть объявлены заранее.
PaperBirdMaster
источник
1
Я полагаю, мы можем ограничить базовый тип enum и для обычных перечислений, при условии, что у нас есть C ++ 11
Sagar Padhye
11
Извините, но этот ответ неверен. «enum class» не имеет ничего общего с возможностью указывать тип. Это независимая функция, которая существует как для обычных перечислений, так и для перечисляемых классов.
февраля
14
Это соглашение: * Перечисление классов является новой функцией в C ++ 11. * Типизированные перечисления являются новой функцией в C ++ 11. Это две отдельные новые функции в C ++ 11. Вы можете использовать и то, и другое, или ни то, ни другое.
октября
2
Я думаю, что Алекс Аллен дает наиболее полное простое объяснение, которое я когда-либо видел в этом блоге на [ cprogramming.com/c++11/… . Традиционное перечисление было хорошо для использования имен вместо целочисленных значений и избегания использования препроцессора #defines, что было хорошо - это добавило ясности. Класс enum удаляет концепцию числового значения перечислителя и вводит объем и строгую типизацию, что увеличивает (ну, может увеличить :-) правильность программы. Это приближает вас на один шаг к мышлению, ориентированному на объект.
Джон Спенсер
2
Кроме того, это всегда забавно, когда вы просматриваете код, и вдруг происходит One Piece .
Джастин Тайм - Восстановить Монику
47

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

Например:

enum class Color1 { red, green, blue };    //this will compile
enum class Color2 { red, green, blue };

enum Color1 { red, green, blue };    //this will not compile 
enum Color2 { red, green, blue };

Что касается основных перечислений, компилятор не сможет различить, redссылается ли на тип Color1или Color2как в приведенном ниже утверждении.

enum Color1 { red, green, blue };   
enum Color2 { red, green, blue };
int x = red;    //Compile time error(which red are you refering to??)
Saksham
источник
1
@Oleksiy Ооо, я не правильно прочитал твой вопрос. Рассмотрим это как дополнение для тех, кто не знал.
Сакшам
все нормально! Я почти забыл об этом
Алексей
конечно, вы бы написали enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE }, легко устраняя проблемы с пространством имен. Аргумент пространства имен - один из трех упомянутых здесь, которые я вообще не покупаю.
Джо Со
2
@Jo Так что это решение - ненужный обходной путь. Enum: enum Color1 { COLOR1_RED, COLOR1_GREEN, COLOR1_BLUE }сравним с классом Enum: enum class Color1 { RED, GREEN, BLUE }. Доступ аналогичен: COLOR1_REDvs Color1::RED, но версия Enum требует, чтобы вы вводили «COLOR1» в каждом значении, что дает больше места для опечаток, которых избегает поведение пространства имен класса enum.
cdgraham
2
Пожалуйста, используйте конструктивную критику . Когда я говорю больше места для опечаток, я имею в виду, когда вы первоначально определяете значения enum Color1, которые компилятор не может поймать, так как это, вероятно, все еще будет «допустимым» именем. Если я пишу RED, GREENи так далее , используя класс перечислимого, чем он не может решить , чтобы , enum Bananaпотому что она требует от вас указать Color1::RED, чтобы получить доступ значения (пространство имен аргументов). Есть все еще хорошие времена, чтобы использовать enum, но поведение пространства имен enum classможет часто быть очень полезным.
cdgraham
20

Перечисления используются для представления набора целочисленных значений.

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

Например:

enum class Animal{Dog, Cat, Tiger};
enum class Pets{Dog, Parrot};

Здесь мы не можем смешивать значения Animal и Pets.

Animal a = Dog;       // Error: which DOG?    
Animal a = Pets::Dog  // Pets::Dog is not an Animal
Alok151290
источник
7

C ++ 11 FAQ упоминает следующие пункты:

обычные перечисления неявно преобразуются в int, вызывая ошибки, когда кто-то не хочет, чтобы перечисление действовало как целое число.

enum color
{
    Red,
    Green,
    Yellow
};

enum class NewColor
{
    Red_1,
    Green_1,
    Yellow_1
};

int main()
{
    //! Implicit conversion is possible
    int i = Red;

    //! Need enum class name followed by access specifier. Ex: NewColor::Red_1
    int j = Red_1; // error C2065: 'Red_1': undeclared identifier

    //! Implicit converison is not possible. Solution Ex: int k = (int)NewColor::Red_1;
    int k = NewColor::Red_1; // error C2440: 'initializing': cannot convert from 'NewColor' to 'int'

    return 0;
}

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

// Header.h

enum vehicle
{
    Car,
    Bus,
    Bike,
    Autorickshow
};

enum FourWheeler
{
    Car,        // error C2365: 'Car': redefinition; previous definition was 'enumerator'
    SmallBus
};

enum class Editor
{
    vim,
    eclipes,
    VisualStudio
};

enum class CppEditor
{
    eclipes,       // No error of redefinitions
    VisualStudio,  // No error of redefinitions
    QtCreator
};

Базовый тип перечисления не может быть указан, что вызывает путаницу, проблемы совместимости и делает невозможным предварительное объявление.

// Header1.h
#include <iostream>

using namespace std;

enum class Port : unsigned char; // Forward declare

class MyClass
{
public:
    void PrintPort(enum class Port p);
};

void MyClass::PrintPort(enum class Port p)
{
    cout << (int)p << endl;
}

,

// Header.h
enum class Port : unsigned char // Declare enum type explicitly
{
    PORT_1 = 0x01,
    PORT_2 = 0x02,
    PORT_3 = 0x04
};

,

// Source.cpp
#include "Header1.h"
#include "Header.h"

using namespace std;
int main()
{
    MyClass m;
    m.PrintPort(Port::PORT_1);

    return 0;
}
Swapnil
источник
C ++ 11 позволяет также набирать «не классовые» перечисления . Проблемы загрязнения пространства имен и т. Д. Все еще существуют. Посмотрите на соответствующие ответы, которые существовали задолго до этого ..
user2864740
7
  1. неявно конвертировать в int
  2. можете выбрать, какой тип лежит в основе
  3. Пространство имен ENUM, чтобы избежать загрязнения
  4. По сравнению с обычным классом, могут быть объявлены вперед, но не имеют методов
Циньшэн Чжан
источник
2

В дополнение к этим другим ответам стоит отметить, что C ++ 20 решает одну из проблем enum class: многословие. Воображая гипотетической enum class, Color.

void foo(Color c)
  switch (c) {
    case Color::Red: ...;
    case Color::Green: ...;
    case Color::Blue: ...;
    // etc
  }
}

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

Тем не менее, в C ++ 20 мы можем использовать, using enumчтобы ввести все имена в перечислении в текущую область, решая проблему.

void foo(Color c)
  using enum Color;
  switch (c) {
    case Red: ...;
    case Green: ...;
    case Blue: ...;
    // etc
  }
}

Так что теперь нет причин не использовать enum class.

Том VH
источник
1

Поскольку, как сказано в других ответах, перечисление классов неявно не конвертируется в int / bool, это также помогает избежать ошибочного кода, такого как:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
Arnaud
источник
2
Чтобы завершить мой предыдущий комментарий, обратите внимание, что у gcc теперь есть предупреждение, называемое -Wint-in-bool-context, которое отлавливает именно такие ошибки.
Арно
0

Одна вещь, которая не была явно упомянута - функция области действия дает вам возможность иметь одинаковое имя для перечисления и метода класса. Например:

class Test
{
public:
   // these call ProcessCommand() internally
   void TakeSnapshot();
   void RestoreSnapshot();
private:
   enum class Command // wouldn't be possible without 'class'
   {
        TakeSnapshot,
        RestoreSnapshot
   };
   void ProcessCommand(Command cmd); // signal the other thread or whatever
};
Миро Кропачек
источник