Чому клас enum віддається перевазі перед звичайним enum?


429

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

Але що це насправді означає?


57
Коли хтось стверджує, що якась програма програмування "зла", він намагається відмовити вас думати про себе.
Піт Бекер

3
@NicolBolas: Це скоріше реторичне запитання, щоб надати відповіді на поширені запитання (чи справді часто задаються питання - це інша історія).
David Rodríguez - dribeas

@David, там обговорюється, чи це повинен бути FAQ, чи не триватиме, який починається тут . Ласкаво просимо.
sbi

17
@ PeteBecker Іноді вони просто намагаються захистити тебе від себе.
пікіровка

geeksforgeeks.org/… Це також гарне місце, щоб зрозуміти enumпроти enum class.
mr_azad

Відповіді:


472

C ++ має два види enum:

  1. enum classес
  2. Рівнина enums

Ось кілька прикладів, як їх оголосити:

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

Яка різниця між двома?

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

  • Звичайний enums - де імена перерахунку знаходяться в тій же області, що і enum, і їх значення неявно перетворюються на цілі числа та інші типи

Приклад:

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 {собака, олень, кішка, птах}
Blasius Secundus

3
@Cat Plus Plus Я розумію, що @Oleksiy каже, що це погано. Моє запитання було не в тому, чи вважав Олексія це погано. Моє запитання було проханням детально пояснити, що в цьому погано. Зокрема, чому Олексій, наприклад, вважає поганим 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) та uncoped ( enum) перераховує, що ще робить enum classкращий вибір ?:

  • Вони не конвертуються неявно int .
  • Вони не забруднюють навколишній простір імен.
  • Вони можуть бути оголошені вперед.

1
Я вважаю, що ми можемо обмежити базовий тип enum і для регулярних перерахунків, якщо у нас є C ++ 11
Sagar Padhye

11
Вибачте, але ця відповідь неправильна. "enum class" не має нічого спільного з можливістю вказувати тип. Це незалежна функція, яка існує як для регулярних переліків, так і для переліків.
rdb

14
Це справа: * Класи Enum - це нова особливість C ++ 11. * Набрані переліки - це нова функція на C ++ 11. Це дві окремі непов'язані нові функції в C ++ 11. Ви можете використовувати і те, і інше, і те, і інше.
rdb

2
Я думаю, що Алекс Аллайн дає найповніше просте пояснення, яке я ще бачив у цьому блозі на сайті [ cprogramming.com/c++11/… . Традиційний перелік був хорошим для використання імен замість цілих значень та уникання використання препроцесора #defines, що було гарною річчю - це додало ясності. enum class видаляє поняття числового значення перелічувача, а також вводить область застосування та сильне введення тексту, що збільшується (ну може збільшити :-) коректність програми. Це зближує вас на крок ближче до орієнтованого на мислення об’єкта.
Джон Спенсер

2
Як осторонь, це завжди цікаво, коли ви переглядаєте код і раптом трапляється One Piece .
Час Джастіна -

47

Основна перевага використання класу enum перед звичайними перерахунками полягає в тому, що у вас можуть бути однакові змінні enum для двох різних переліків і все одно можете їх вирішити (що було зазначено як безпечний тип ОП )

Наприклад:

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як у hte нижче оператора.

enum Color1 { red, green, blue };   
enum Color2 { red, green, blue };
int x = red;    //Compile time error(which red are you refering to??)

1
@Oleksiy О, я не правильно прочитав ваше запитання. Розглянемо це як доповнення для тих, хто не знав.
Сакшам

нічого страшного! Я майже забув про це
Олексій

Звичайно, ви б писали enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE }, легко усуваючи проблеми простору імен. Аргумент простору імен є одним із трьох згаданих тут, які я взагалі не купую.
Jo So

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 a = Dog;       // Error: which DOG?    
Animal a = Pets::Dog  // Pets::Dog is not an Animal

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

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.


1

Оскільки, як сказано в інших відповідях, enum класів не конвертована неявно в int / bool, це також допомагає уникнути помилки коду, як:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning

2
Щоб завершити мій попередній коментар, зауважте, що тепер gcc має попередження під назвою -Wint-in-bool-context, яке спричинить саме такі помилки.
Арно

0

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

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
};
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.