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

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

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


person Oleksiy    schedule 20.08.2013    source источник
comment
Когда кто-то заявляет, что какая-то программная конструкция является злом, они пытаются отговорить вас думать самостоятельно.   -  person Pete Becker    schedule 20.08.2013
comment
@NicolBolas: Это скорее реторический вопрос, чтобы дать ответ на часто задаваемый вопрос (действительно ли это спрашивают Часто - это другая история).   -  person David Rodríguez - dribeas    schedule 20.08.2013
comment
@David, идет обсуждение, должно ли это быть FAQ или нет, которое начинается здесь. Добро пожаловать.   -  person sbi    schedule 21.08.2013
comment
@PeteBecker Иногда они просто пытаются защитить вас от самого себя.   -  person piccy    schedule 11.05.2018
comment
geeksforgeeks.org/ Это также хорошее место, чтобы понять enum vs enum class.   -  person mr_azad    schedule 20.04.2020
comment
Два слова: загрязнение пространства имен   -  person wcochran    schedule 10.04.2021


Ответы (9)


В C ++ есть два типа enum:

  1. enum classes
  2. Обычный enums

Вот пара примеров их объявления:

 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 classes следует предпочесть, потому что они вызывают меньше неожиданностей, которые потенциально могут привести к ошибкам.

person Oleksiy    schedule 20.08.2013
comment
Хороший пример ... есть ли способ объединить безопасность типа версии класса с продвижением пространства имен версии enum? То есть, если у меня есть класс A с состоянием, и я создаю enum class State { online, offline }; как дочерний элемент класса A, я хотел бы сделать state == online проверки внутри A вместо _6 _... возможно ли это? - person mark; 20.08.2013
comment
Неа. Продвижение пространства имен - это Плохая Вещь ™, и половина оправдания для enum class заключалась в его устранении. - person Puppy; 20.08.2013
comment
В C ++ 11 вы также можете использовать явно типизированные перечисления, например enum Animal: unsigned int {dog, deer, cat, bird} - person Blasius Secundus; 20.08.2013
comment
Примечание: вдобавок ко всему у нас теперь есть явный базовый тип, доступный как для enum, что позволяет прямое объявление и более строгий контроль над компоновкой класса :) - person Matthieu M.; 20.08.2013
comment
Почему Color color = Color :: red - пример неправильного использования простых перечислений? Это плохо просто потому, что использует простые перечисления или что-то еще? - person chux - Reinstate Monica; 20.08.2013
comment
@chux Плохие перечисления - это весь блок, а не только первый оператор. - person Cat Plus Plus; 21.08.2013
comment
@Cat Plus Plus Я понимаю, что @Oleksiy говорит, что это плохо. Я не спрашивал, думал ли Алексей, что это плохо. Мой вопрос был просьбой подробно рассказать, что в этом плохого. В частности, почему, например, Алексей считает плохим Color color = Color::red. - person chux - Reinstate Monica; 21.08.2013
comment
@chux: Ох, опять же, посмотрите на код между этим комментарием и примерами правильного использования классов enum (безопасно), а не только на первый оператор. - person Cat Plus Plus; 21.08.2013
comment
@Cat Plus Plus Таким образом, в примере bad не возникает до строки if (color == Card::red_card), на 4 строки позже комментария (который, как я вижу, теперь применяется к первой половине блока). 2 строки блока дают плохие примеры. Первые 3 строки не проблема. Весь блок, почему простые перечисления плохи, бросил меня, поскольку я думал, вы имели в виду, что с ними тоже что-то не так. Теперь я понимаю, это просто подстава. В любом случае спасибо за отзыв. - person chux - Reinstate Monica; 21.08.2013
comment
@Oleksiy Вы приводите примеры использования разных enum типов. Но почему, ПОЧЕМУ вы не объясняете точно (как указано в вопросе) почему это плохо и это хорошо < / я>. - person TobiMcNamobi; 12.10.2015
comment
Другое не упомянутое преимущество заключается в том, что класс enum может быть объявлен вперед. - person user2672165; 04.04.2018
comment
потому что они вызывают меньше сюрпризов, которые потенциально могут привести к ошибкам. Я не понимаю. Что было большим сюрпризом в этом коде? - person Mathematician; 30.10.2018
comment
Я действительно удивлен, что ключевое слово using не было скорректировано в стандарте, чтобы позволить using enum class Foo; продвигать всех членов Foo в текущую область видимости ... Разве это не было бы очень полезно, скажем, в функции токенизатора? - person Jimmio92; 23.08.2019

Из часто задаваемых вопросов по C ++ 11 Бьярна Страуструпа:

enum classes («новые перечисления», «строгие перечисления») решают три проблемы с традиционными перечислениями 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 class) и без области действия (enum), что еще делает enum class лучшим выбором ?:

  • Они не конвертируются в int неявно.
  • Они не загрязняют окружающее пространство имен.
  • Они могут быть заявлены заранее.
person PaperBirdMaster    schedule 20.08.2013
comment
Я полагаю, мы можем ограничить базовый тип enum и для обычных перечислений, если у нас есть C ++ 11 - person Sagar Padhye; 28.12.2015
comment
Извините, но это неверный ответ. Класс enum не имеет ничего общего с возможностью указать тип. Это независимая функция, которая существует как для обычных перечислений, так и для классов перечислений. - person rdb; 06.05.2016
comment
@rdb, возможно, я ошибаюсь и мне нужно исправить свой ответ, как я могу указать базовый тип для традиционного enum? Кстати, на странице ссылки на часто задаваемые вопросы вы можете прочитать это: базовый тип перечисления не может быть указан, что вызывает путаницу, проблемы совместимости и делает невозможным предварительное объявление. - person PaperBirdMaster; 06.05.2016
comment
Вот так: enum A : unsigned char {...}. В статье описываются версии до C ++ 11, в которых это было невозможно. Но это не имеет ничего общего с классами перечисления. - person rdb; 06.05.2016
comment
В этом суть: * Классы Enum - это новая функция в C ++ 11. * Типизированные перечисления - это новая функция в C ++ 11. Это две отдельные не связанные между собой новые функции в C ++ 11. Вы можете использовать и то, и другое, или то, и другое, или ни то, ни другое. - person rdb; 06.05.2016
comment
Я думаю, что Алекс Аллен дает наиболее полное простое объяснение, которое я когда-либо видел в этом блоге по адресу [cprogramming.com/c++11/. Традиционное перечисление хорошо подходит для использования имен вместо целочисленных значений и избегания использования препроцессора #defines, что было хорошо - это добавляло ясности. enum class удаляет понятие числового значения перечислителя и вводит область видимости и строгую типизацию, которая увеличивает (ну, может увеличить :-) корректность программы. Это на шаг приближает вас к объектно-ориентированному мышлению. - person Jon Spencer; 07.01.2017
comment
вау, это лучшее из обоих миров; по умолчанию безопасный тип, но не несовместим с двоичным кодом, несовместимый с C. - person Dmitry; 01.04.2018
comment
Однако я считаю, что вы должны сами определять побитовые операции, что добавляет много шаблонного кода. - person user2672165; 04.04.2018
comment
Кстати, всегда забавно, когда вы просматриваете код и внезапно случается One Piece. - person Justin Time - Reinstate Monica; 25.09.2019

Основное преимущество использования класса enum по сравнению с обычными перечислениями заключается в том, что у вас могут быть одинаковые переменные перечисления для двух разных перечислений, и вы все равно можете их разрешать (что было упомянуто 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??)
person Saksham    schedule 20.08.2013
comment
@Oleksiy Ohh Я неправильно прочитал ваш вопрос. Считайте это дополнением для тех, кто не знал. - person Saksham; 20.08.2013
comment
Ничего страшного! Я чуть не забыл об этом - person Oleksiy; 20.08.2013
comment
конечно, вы должны написать enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE }, чтобы легко избежать проблем с пространством имен. Аргумент пространства имен - это один из трех упомянутых здесь аргументов, которые я вообще не покупаю. - person Jo So; 26.10.2018
comment
@Jo Итак, это решение - ненужный обходной путь. Enum: enum Color1 { COLOR1_RED, COLOR1_GREEN, COLOR1_BLUE } сравним с классом Enum: enum class Color1 { RED, GREEN, BLUE }. Доступ аналогичен: COLOR1_RED vs Color1::RED, но версия Enum требует, чтобы вы вводили COLOR1 в каждое значение, что дает больше места для опечаток, которых избегает поведение пространства имен класса enum. - person cdgraham; 05.03.2019
comment
which gives more room for typos. Извините, но это самый страшный аргумент, который я слышал в 2019 году. Пора немного попрактиковаться и почувствовать, что имеет значение. Я гарантирую вам, что люди не так быстро распознают идентификаторы, как компиляторы. Так что лучше не бросайте им вызов. Это убедительный аргумент. - person Jo So; 07.03.2019
comment
Чтобы уточнить: это дразня, потому что, если у вас есть опечатка в префиксе, вероятность того, что компилятор поймает ее, составляет 99,99%. Что касается оставшихся 0,01%, скорее всего, программист заметит некорректное поведение программы. - person Jo So; 07.03.2019
comment
@cdgraham Теперь подумайте: если вы напишете RED, GREEN и так далее (используя класс перечисления), не намного ли выше вероятность того, что автоматическое разрешение разрешит перечисление enum Banana вместо перечисления Color1? Пожалуйста, научитесь быть критичным и постарайтесь быть объективным. - person Jo So; 07.03.2019
comment
Используйте конструктивную критику. Когда я говорю больше места для опечаток, я имею в виду, когда вы изначально определяете значения enum Color1, которые компилятор не может уловить, поскольку это, скорее всего, все еще будет «допустимым» именем. Если я напишу RED, GREEN и так далее, используя класс enum, он не сможет разрешить enum Banana, потому что для доступа к значению (аргументу пространства имен) требуется указать Color1::RED. Еще бывают хорошие времена для использования enum, но поведение enum class в пространстве имен часто может быть очень полезным. - person cdgraham; 07.03.2019

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

Ключевое слово 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
person Alok151290    schedule 04.02.2015

  1. не неявно преобразовывать в int
  2. можете выбрать, какой тип лежит в основе
  3. ENUM, чтобы избежать загрязнения
  4. По сравнению с обычным классом, может быть объявлен вперед, но не имеет методов
person Qinsheng Zhang    schedule 06.06.2019

Помимо этих других ответов, стоит отметить, что 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.

person Tom VH    schedule 14.05.2020

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;
}
person Swapnil    schedule 02.08.2018
comment
C ++ 11 также позволяет типизировать перечисления, не относящиеся к классам. Проблемы с загрязнением пространства имен и т. Д. Все еще существуют. Взгляните на соответствующие ответы, которые существовали задолго до этого ... - person user2864740; 17.02.2020

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

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

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

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
};
person Miro Kropacek    schedule 13.05.2019