Я слышал, что несколько человек рекомендуют использовать классы enum в С++ из-за безопасности типа.
Но что это значит?
Я слышал, что несколько человек рекомендуют использовать классы enum в С++ из-за безопасности типа.
Но что это значит?
С++ имеет два типа enum
:
enum class
эсenum
Вот несколько примеров, как объявить их:
enum class Color { red, green, blue }; // enum class
enum Animal { dog, cat, bird, human }; // plain enum
В чем разница между двумя?
enum class
es - имена перечислителя перечислены локально, и их значения неявно не преобразуются в другие типы (например, другие enum
или int
)
Plain 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
es должны быть предпочтительнее, потому что они вызывают меньше неожиданностей, которые потенциально могут привести к ошибкам.
Из Bjarne Stroustrup C++ 11 Часто задаваемые вопросы:
Класс
enum class
es ("new enums", "strong enums") адресует три проблемы с традиционными C++ перечислениями:
- обычные перечисления неявно преобразуются в int, вызывая ошибки, когда кто-то не хочет, чтобы перечисление выполнялось как целое число.
- обычные перечисления экспортируют свои счетчики в окружающую область, вызывая столкновения имен.
- базовый тип
enum
не может быть указан, что вызывает путаницу, проблемы совместимости и делает невозможным объявление вперед.Новые перечисления являются "enum class", потому что они объединяют аспекты традиционных перечислений (значения имен) с аспектами классов (охваченные члены и отсутствие конверсий).
Таким образом, как упоминалось другими пользователями, "сильные перечисления" сделают код более безопасным.
Основной тип "классического" enum
должен быть целым типом, достаточно большим, чтобы соответствовать всем значениям enum
; это обычно int
. Также каждый перечисленный тип должен быть совместим с char
или целочисленным типом signed/unsigned.
Это широкое описание, что такое 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- E_MY_FAVOURITE_FRUITS
типе... но там нет никаких гарантий: компилятор может выбрать unsigned char
или int
или short
, любой из этих типов является большим достаточно, чтобы соответствовать всем значениям, указанным в enum
. Добавление поля E_MY_FAVOURITE_FRUITS_FORCE8
является бременем и не заставляет компилятор делать какой-либо выбор относительно базового типа enum
.
Если есть какой-то фрагмент кода, который полагается на размер шрифта и/или предполагает, что E_MY_FAVOURITE_FRUITS
будет иметь некоторую ширину (например: процедуры сериализации), этот код может вести себя каким-то странным образом в зависимости от E_MY_FAVOURITE_FRUITS
компилятора.
И чтобы усугубить ситуацию, если какой-то помощник неохотно добавляет новое значение нашему enum
:
E_DEVIL_FRUIT = 0x100, // New fruit, with value greater than 8bits
Компилятор не жалуется на это! Он просто изменяет размер типа, чтобы соответствовать всем значениям enum
(предполагая, что компилятор использовал наименьший возможный тип, что является предположением, которого мы не можем сделать). Это простое и неосторожное дополнение к enum
может привести к разрыву связанного кода.
Поскольку C++ 11 можно указать базовый тип для enum
и enum class
(thanks 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 class
(enum class
) и unscoped (enum
) enums, что еще делает enum class
лучшим выбором?
int
.Основное преимущество использования класса 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
, как в инструкции hte ниже.
enum Color1 { red, green, blue };
enum Color2 { red, green, blue };
int x = red; //Compile time error(which red are you refering to??)
Перечисления используются для представления набора целых значений.
Ключевое слово 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
С++ 11 Часто задаваемые вопросы упоминаются ниже:
обычные перечисления неявно преобразуются в 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;
}
Поскольку, как сказано в других ответах, перечисление классов неявно не конвертируется в int/bool, это также помогает избежать ошибочного кода, такого как:
enum MyEnum {
Value1,
Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
Одна вещь, которая не была явно упомянута - функция области действия дает вам возможность иметь одинаковое имя для перечисления и метода класса. Например:
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
};
... используйте #defines, и все будет хорошо