Класи-перерахування (enum class)
Класи-перерахування (enum class)
Тихий баґ, який проходить компіляцію
Ця програма компілюється без жодного попередження. Вона запускається. Вона видає результат. І вона хибна:
#include <iostream>
int main()
{
enum Fruit { LEMON, KIWI };
enum Color { PINK, GRAY };
Fruit fruit = LEMON;
Color color = PINK;
if (fruit == color)
std::cout << "fruit == color\n"; // ← ця гілка виконується!
else
std::cout << "fruit != color\n";
return 0;
}
Фрукт лимон дорівнює кольору рожевий. Логіка абсурдна — але компілятор мовчить. Чому?
Обидва LEMON і PINK — це перші елементи своїх перерахувань, тому обидва мають значення 0. Незахищений enum неявно конвертується в int, і в момент порівняння відбувається не «чи ці два об'єкти однієї природи», а «чи рівні два числа 0». Відповідь — так. Помилка залишається в програмі непоміченою.
Це не гіпотетичний крайній випадок. Саме такі помилки знаходять при рефакторингу великих кодових баз, які роками накопичували подібні «незахищені» порівняння між перерахуваннями різної семантики. Стандарт C++11 запропонував системне рішення: клас-перерахування (enum class).
enum: енумератори потрапляють у той самий простір імен, що і саме перерахування, а їх значення неявно конвертуються в цілі числа при будь-якому порівнянні або арифметиці. Саме ці два факти разом уможливлюють хибно-успішну компіляцію fruit == color.Що таке «захищене перерахування»
Клас-перерахування (scoped enumeration, або enum class) — це конструкція C++11, яка розширює звичайний enum двома критичними гарантіями:
- Власний простір імен: енумератори існують виключно всередині свого перерахування і недоступні ззовні без явної кваліфікації.
- Відсутність неявних конвертацій: значення
enum classне конвертується вintавтоматично — ані при порівнянні, ані при арифметичних операціях.
Синтаксично від звичайного enum відрізняється лише ключовим словом class після enum:
enum class Direction
{
North,
South,
East,
West,
};
enum class можна писати enum struct — це абсолютні синоніми з ідентичною семантикою. У практиці промислового коду найчастіше зустрічається саме enum class, тому ми будемо дотримуватись цього запису.Оператор розширення імені (::)
Доступ до значень enum class здійснюється виключно через оператор розширення імені (scope resolution operator) ::. Прямий доступ до енумератора без префіксу перерахування — це помилка компіляції:
#include <iostream>
enum class Direction
{
North,
South,
East,
West,
};
int main()
{
Direction playerDir = Direction::North; // ✅ правильно
// Direction dir = North; // ❌ помилка: 'North' was not declared in this scope
std::cout << "Напрямок задано.\n";
return 0;
}
Така вимога на перший погляд може здатися надмірно суворою — доводиться писати більше. Але саме ця «суворість» усуває всі конфлікти імен: тепер можна мати Direction::North і Compass::North в одній програмі без жодних проблем, тоді як зі звичайним enum такі однойменні константи неминуче конфліктували б у просторі імен.
Порівняємо два підходи до оголошення схожих перерахувань:
enum Fruit { LEMON, KIWI, MANGO };
enum Color { PINK, GRAY };
// Спроба додати Color з Yellow:
// enum Color2 { YELLOW }; // ✅ інша назва, але...
// enum AnotherFruit { LEMON }; // ❌ 'LEMON' вже оголошено!
Енумератори потрапляють у глобальний простір, конфлікти неминучі при зростанні кодової бази.
enum class Fruit { Lemon, Kiwi, Mango };
enum class Color { Pink, Gray };
// Обидва мають Lemon/Pink — жодного конфлікту:
Fruit f = Fruit::Lemon;
Color c = Color::Pink;
Кожне перерахування — окремий простір імен. Однакові імена в різних enum class — норма.
Суворі правила типізації
Головна перевага enum class — система типів: компілятор вважає кожне перерахування окремим, несумісним типом. Спробуємо відтворити початковий баґ з використанням enum class:
#include <iostream>
int main()
{
enum class Fruit { Lemon, Kiwi };
enum class Color { Pink, Gray };
Fruit fruit = Fruit::Lemon;
Color color = Color::Pink;
if (fruit == color) // ❌ помилка компіляції!
std::cout << "рівні\n";
return 0;
}
Компілятор (GCC) видасть:
Відмінно. Помилка, яка раніше непомітно проходила крізь компілятор і жила в продакшені, тепер стає помилкою компіляції — найкращим місцем для виявлення будь-якої проблеми.
Що дозволено, а що ні
✅ Дозволено
- Порівняння двох значень одного
enum class - Присвоєння значення того самого типу
- Передача як аргумент функції відповідного типу
- Використання у
switch-операторі
❌ Заборонено
- Порівняння значень різних
enum class - Неявна конвертація в
int - Неявна конвертація з
int - Арифметичні операції безпосередньо над
enum class
Порівняння всередині одного enum class — природно і очевидно:
#include <iostream>
enum class Color { Pink, Gray, Blue };
int main()
{
Color selected = Color::Pink;
if (selected == Color::Pink)
std::cout << "Рожевий!\n";
else if (selected == Color::Gray)
std::cout << "Сірий!\n";
return 0;
}
Явна конвертація через static_cast
Оскільки неявна конвертація enum class → int заборонена, спроба вивести значення через std::cout дасть помилку компіляції:
enum class Color { Pink, Gray };
Color c = Color::Gray;
std::cout << c; // ❌ помилка: no match for 'operator<<'
Якщо числове значення все ж потрібне, використовується явна конвертація (explicit cast) через static_cast:
#include <iostream>
enum class Color { Pink, Gray };
int main()
{
Color c = Color::Gray;
// std::cout << c; // ❌ не компілюється
std::cout << static_cast<int>(c); // ✅ виведе: 1
return 0;
}
Зворотна конвертація: небезпека UB
Конвертація int → enum class через static_cast синтаксично дозволена, але несе приховану небезпеку:
enum class Color { Pink = 0, Gray = 1 };
int input = 99;
Color c = static_cast<Color>(input); // ✅ компілюється
// але c має значення, якого не існує в Color — це UB!
static_cast<EnumClass>(value), не відповідає жодному оголошеному енумератору, поведінка не визначена (undefined behavior). Це особливо небезпечно при роботі з даними, що надходять від користувача, з мережі або з файлу. Завжди валідуйте вхідне значення перед конвертацією.Безпечна конвертація через std::underlying_type_t
Для отримання числового значення без ризику помилки існує більш виразний і семантично точний спосіб — використати std::underlying_type_t із заголовка <type_traits>. Цей підхід автоматично визначає базовий тип перерахування:
#include <iostream>
#include <type_traits>
enum class Permission { Read, Write, Execute };
int main()
{
Permission p = Permission::Write;
auto numericValue = static_cast<std::underlying_type_t<Permission>>(p);
std::cout << "Числове значення: " << numericValue << "\n"; // 1
return 0;
}
Перевага std::underlying_type_t<Permission> над прямим int полягає в тому, що цей вираз завжди дає правильний тип — навіть якщо базовий тип перерахування буде змінено (наприклад, з int на uint8_t). Код залишається коректним без жодних змін.
Задання базового типу
За замовчуванням enum class зберігає значення у типі int (4 байти). Але стандарт C++11 дозволяє явно задати базовий тип (underlying type) — будь-який цілочисельний тип:
enum class Direction : uint8_t
{
North,
South,
East,
West,
};
Двокрапка після імені перерахування вказує базовий тип. У цьому прикладі кожен енумератор займає лише 1 байт замість 4.
Навіщо задавати базовий тип
Причина 1 — економія пам'яті у вбудованих системах. Мікроконтролери класу Arduino або STM32 мають обмежену оперативну пам'ять (від кількох сотень байт до кількох кілобайт). Якщо масив зі станами пристроїв містить 1000 елементів, різниця між uint8_t та int становить 3 кілобайти — цілком суттєво.
Причина 2 — мережеві протоколи. Якщо ви серіалізуєте дані для передачі по мережі або записи у файл, розмір поля у протоколі фіксований. uint16_t або uint8_t дозволяє точно відобразити структуру пакету у вашому C++-типі.
Причина 3 — сумісність з C API. Деякі C-бібліотеки очікують конкретні цілочисельні типи (наприклад, uint32_t для прапорців).
Перевіримо розміри:
#include <iostream>
enum class StatusDefault { Ok, Error };
enum class StatusSmall : uint8_t { Ok, Error };
enum class StatusLarge : uint32_t { Ok, Error };
int main()
{
std::cout << "sizeof(StatusDefault) = " << sizeof(StatusDefault) << " байт\n";
std::cout << "sizeof(StatusSmall) = " << sizeof(StatusSmall) << " байт\n";
std::cout << "sizeof(StatusLarge) = " << sizeof(StatusLarge) << " байт\n";
return 0;
}
enum class у switch-операторі
Конструкція switch з enum class повністю аналогічна до звичайного enum, але у кожному case потрібна кваліфікація через :::
#include <iostream>
enum class Direction : uint8_t
{
North,
South,
East,
West,
};
const char* directionName(Direction dir)
{
switch (dir)
{
case Direction::North: return "Північ";
case Direction::South: return "Південь";
case Direction::East: return "Схід";
case Direction::West: return "Захід";
default: return "Невідомо";
}
}
int main()
{
Direction current = Direction::East;
std::cout << "Поточний напрямок: " << directionName(current) << "\n";
return 0;
}
Попередження компілятора про неповний switch
Один з найцінніших ефектів роботи зі switch над enum class — попередження компілятора про непокриті гілки. Якщо у switch відсутній case для якогось значення, більшість компіляторів (GCC з -Wall, Clang) видадуть:
warning: enumeration value 'West' not handled in switch
Це попередження — ваш безкоштовний захист від ситуації, коли ви додали новий енумератор, але забули оновити всі відповідні switch-конструкції у програмі. Незахищений enum також генерує таке попередження, але воно легше пропускається через шум від «дозволених» неявних порівнянь. З enum class картина чистіша.
switch навмисно пропущені і ви хочете пригнітити попередження, використовуйте атрибут [[fallthrough]] або додайте порожній default: з коментарем про навмисне рішення. Це кращий стиль, ніж мовчати і залишати попередження незадокументованим.Порівняльна таблиця: enum проти enum class
| Властивість | enum | enum class |
|---|---|---|
| Область видимості енумераторів | Глобальна (витік) | Локальна (ізольована) |
Неявна конвертація → int | ✅ | ❌ |
| Порівняння різних перерахувань | ✅ (баґ!) | ❌ (помилка компіляції) |
| Задання базового типу | Частково (C++11) | ✅ Повністю |
Попередження про неповний switch | ✅ | ✅ |
| Конфлікти імен між перерахуваннями | Можливі | Неможливі |
| Рекомендований у C++11+ | ❌ | ✅ |
enum у сучасному C++ — взаємодія зі старим C-кодом або API, яке явно очікує нескваліфіковані константи. У всіх нових кодах слід надавати перевагу enum class.Практика
Рівень 1 — Базовий
Дано такий код зі звичайним enum:
enum Season { SPRING, SUMMER, AUTUMN, WINTER };
Season current = SUMMER;
if (current == SUMMER)
std::cout << "Літо!\n";
Переробіть Season у enum class. Виправте всі звернення до енумераторів, додавши необхідний префікс Season::. Переконайтеся, що код компілюється.
enum class Weekday : uint8_t зі значеннями Monday через Sunday. Напишіть функцію const char* weekdayName(Weekday day), яка повертає назву дня українською. У main() виведіть усі сім назв, використовуючи static_cast<Weekday> для перебору від 0 до 6.Рівень 2 — Логіка та системи
Оголосіть enum class Permission : uint8_t зі значеннями Read (0), Write (1), Execute (2), Admin (3).
Напишіть функцію bool hasPermission(Permission userLevel, Permission required), яка повертає true, якщо userLevel числово не менший за required. Продемонструйте: адміністратор має всі права, звичайний користувач може читати, але не може виконувати.
Для отримання числових значень використовуйте std::underlying_type_t<Permission>.
enum class Direction : uint8_t із чотирма сторонами світу. Реалізуйте функцію Direction opposite(Direction dir), яка повертає протилежний напрямок: North ↔ South, East ↔ West. Спробуйте зробити це через switch і переконайтеся, що компілятор попереджає про неповний switch, якщо ви пропустите хоч один case.Рівень 3 — Архітектура
Реалізуйте enum class HttpStatus : uint16_t з наступними значеннями:
Ok = 200, Created = 201,
BadRequest = 400, Unauthorized = 401, Forbidden = 403, NotFound = 404,
InternalServerError = 500, ServiceUnavailable = 503
Напишіть такі функції:
const char* statusText(HttpStatus)— текстовий опис статусу.bool isSuccess(HttpStatus)—trueдля 2xx.bool isClientError(HttpStatus)—trueдля 4xx.bool isServerError(HttpStatus)—trueдля 5xx.
У main() симулюйте декілька відповідей сервера і виводьте для кожної: код (через static_cast<uint16_t>), текст та категорію.
Підказка: Для перевірки діапазону використовуйте static_cast<uint16_t>(status) >= 200 && ....
Резюме
enum class — це відповідь стандарту C++11 на реальну проблему: незахищені перерахування дозволяли порівнювати «фрукти з кольорами», і компілятор мовчав. Два ключових доповнення — власний простір імен і заборона неявних конвертацій — перетворюють перерахування на повноцінний суворо типізований тип.
Ключові висновки цього розділу:
enum classоголошується додаванням ключового словаclassпісляenum;enum struct— повний синонім.- Доступ до значень лише через
:::Direction::North, не простоNorth. - Порівняння значень різних
enum class— помилка компіляції, а не тихий баґ. - Конвертація в
intвимагає явногоstatic_cast<int>(value); для незалежності від базового типу —static_cast<std::underlying_type_t<MyEnum>>(value). - Базовий тип задається через двокрапку:
enum class X : uint8_t { ... }— корисно для протоколів і вбудованих систем. - У
switchкоженcaseвимагає повної кваліфікації; компілятор попереджає про непокриті гілки.
enum class за замовчуванням. Повертайтесь до незахищеного enum лише тоді, коли цього явно вимагає сумісність із зовнішнім кодом.Перерахування (enum)
Дізнайтеся, що таке перелічуваний тип даних у C++, як оголошувати та використовувати enum, які є внутрішні механізми роботи перерахувань та як вони роблять код читабельнішим і безпечнішим.
Псевдоніми типів (typedef і using)
Дізнайтеся, навіщо потрібні псевдоніми типів у C++, чим відрізняються typedef та using, як спрощувати складні сигнатури та забезпечувати кросплатформність через стандартні типи з <cstdint>.