9.3 – Классы перечислений
Хотя перечисляемые типы в C++ являются отдельными типами, они не типобезопасны и в некоторых случаях позволяют делать вещи, которые бессмысленны. Рассмотрим следующий случай:
#include <iostream>
int main()
{
enum Color
{
color_red, // color_red помещается в ту же область видимости, что и Color
color_blue
};
enum Fruit
{
fruit_banana, // fruit_banana находится в той же области видимости, что и Fruit
fruit_apple
};
// Color и color_red доступны в одной области (префикс не нужен)
Color color{ color_red };
// Fruit и fruit_banana доступны в одной области (префикс не нужен)
Fruit fruit{ fruit_banana };
if (color == fruit) // Компилятор сравнит a и b как целые числа
std::cout << "color and fruit are equal\n"; // и обнаруживаем, что они равны!
else
std::cout << "color and fruit are not equal\n";
return 0;
}
Когда C++ сравнивает color
и fruit
, он неявно преобразует color
и fruit
в целые числа и сравнивает эти целые числа. Поскольку и color
, и fruit
были установлены в значения перечислителей, которые оцениваются как 0, это означает, что в приведенном выше примере color
будет равен fruit
. Это определенно не то, что хотелось бы, поскольку color
и fruit
взяты из разных перечислений и не предназначены для сравнения! Сравнение перечислителей из разных перечислений невозможно предотвратить помощью стандартных перечислителей.
C++11 определяет новую концепцию, класс перечисления (enum class, также называемый перечислением с ограниченной областью видимости), который делает перечисления как строго типизированными, так и строго ограниченными по области видимости. Чтобы создать класс перечисления, мы используем ключевое слово class
после ключевого слова enum
. Вот пример:
#include <iostream>
int main()
{
// "enum class" определяет это как перечисление с ограниченной областью
// видимости вместо стандартного перечисления
enum class Color
{
red, // red находится внутри области видимости Color
blue
};
enum class Fruit
{
banana, // banana находится внутри области видимости Fruit
apple
};
// примечание: red больше не доступен напрямую, мы должны использовать Color::red
Color color{ Color::red };
// примечание: banana больше не доступен напрямую, мы должны использовать Fruit::banana
Fruit fruit{ Fruit::banana };
// здесь ошибка компиляции, так как компилятор не знает,
// как сравнивать разные типы Color и Fruit
if (color == fruit)
std::cout << "color and fruit are equal\n";
else
std::cout << "color and fruit are not equal\n";
return 0;
}
При обычных перечислениях перечислители помещаются в ту же область видимости, что и само перечисление, поэтому обычно вы можете получить доступ к перечислителям напрямую (например, red
). Однако с классами перечисления строгие правила области видимости означают, что все перечислители считаются частью перечисления, поэтому для доступа к перечислителю необходимо использовать квалификатор области видимости (например, Color::red
). Это помогает снизить загрязнение пространства имен и вероятность конфликтов имен.
Поскольку перечислители являются частью класса перечисления, нет необходимости добавлять префиксы к именам перечислителей (например, можно называть их "red" вместо "color_red", поскольку имя Color::color_red
является избыточным).
Правила строгой типизации означают, что каждый класс перечисления считается уникальным типом. Это означает, что компилятор не будет неявно сравнивать перечислители из разных перечислений. Если вы попытаетесь это сделать, как показано в примере выше, компилятор выдаст ошибку.
Однако обратите внимание, что вы всё еще можете сравнивать перечислители из одного класса перечисления (поскольку они одного типа):
#include <iostream>
int main()
{
enum class Color
{
red,
blue
};
Color color{ Color::red };
if (color == Color::red) // это нормально
std::cout << "The color is red!\n";
else if (color == Color::blue)
std::cout << "The color is blue!\n";
return 0;
}
С классами перечисления компилятор больше не будет неявно преобразовывать значения перечислителей в целые числа. В основном это хорошо. Однако бывают случаи, когда это полезно. В этих случаях вы можете явно преобразовать перечислитель класса перечисления в целочисленное значение, используя static_cast
в int
:
#include <iostream>
int main()
{
enum class Color
{
red,
blue
};
Color color{ Color::blue };
std::cout << color; // не сработает, потому что нет неявного преобразования в int
std::cout << static_cast<int>(color); // напечатает 1
return 0;
}
Нет особых причин использовать обычные перечисляемые типы вместо классов перечислений.
Обратите внимание, что ключевое слово class
, наряду с ключевым словом static
, является одним из наиболее перегруженных ключевых слов в языке C++ и в зависимости от контекста может иметь разное значение. Хотя классы перечислений используют ключевое слово class
, они не считаются «классами» в традиционном смысле C++. О реальных классах мы поговорим позже.
Кроме того, на случай, если вы когда-нибудь столкнетесь с этим, "enum struct" эквивалентно "enum class". Но этот вариант не рекомендуется и обычно не используется.