C++

Класи-перерахування (enum class)

Дізнайтеся, чому незахищений enum є небезпечним, як enum class вирішує проблеми простору імен і неявних конвертацій, та коли використовувати static_cast і базовий тип перерахування.

Класи-перерахування (enum class)

Тихий баґ, який проходить компіляцію

Ця програма компілюється без жодного попередження. Вона запускається. Вона видає результат. І вона хибна:

Main.cpp
#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;
}
./Main
$ ./Main
fruit == color
Execution finished with exit code 0.

Фрукт лимон дорівнює кольору рожевий. Логіка абсурдна — але компілятор мовчить. Чому?

Обидва LEMON і PINK — це перші елементи своїх перерахувань, тому обидва мають значення 0. Незахищений enum неявно конвертується в int, і в момент порівняння відбувається не «чи ці два об'єкти однієї природи», а «чи рівні два числа 0». Відповідь — так. Помилка залишається в програмі непоміченою.

Це не гіпотетичний крайній випадок. Саме такі помилки знаходять при рефакторингу великих кодових баз, які роками накопичували подібні «незахищені» порівняння між перерахуваннями різної семантики. Стандарт C++11 запропонував системне рішення: клас-перерахування (enum class).

Описана вище вразливість є прямим наслідком двох властивостей незахищеного enum: енумератори потрапляють у той самий простір імен, що і саме перерахування, а їх значення неявно конвертуються в цілі числа при будь-якому порівнянні або арифметиці. Саме ці два факти разом уможливлюють хибно-успішну компіляцію fruit == color.

Що таке «захищене перерахування»

Клас-перерахування (scoped enumeration, або enum class) — це конструкція C++11, яка розширює звичайний enum двома критичними гарантіями:

  1. Власний простір імен: енумератори існують виключно всередині свого перерахування і недоступні ззовні без явної кваліфікації.
  2. Відсутність неявних конвертацій: значення enum class не конвертується в int автоматично — ані при порівнянні, ані при арифметичних операціях.

Синтаксично від звичайного enum відрізняється лише ключовим словом class після enum:

enum class Direction
{
    North,
    South,
    East,
    West,
};
Замість enum class можна писати enum struct — це абсолютні синоніми з ідентичною семантикою. У практиці промислового коду найчастіше зустрічається саме enum class, тому ми будемо дотримуватись цього запису.

Оператор розширення імені (::)

Доступ до значень enum class здійснюється виключно через оператор розширення імені (scope resolution operator) ::. Прямий доступ до енумератора без префіксу перерахування — це помилка компіляції:

Main.cpp
#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система типів: компілятор вважає кожне перерахування окремим, несумісним типом. Спробуємо відтворити початковий баґ з використанням enum class:

Main.cpp
#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) видасть:

g++ -std=c++17 Main.cpp
$ g++ -std=c++17 Main.cpp
error: no match for 'operator==' (operand types are 'Fruit' and 'Color')
if (fruit == color)
~~~~~~ ^~ ~~~~~
Compilation failed.

Відмінно. Помилка, яка раніше непомітно проходила крізь компілятор і жила в продакшені, тепер стає помилкою компіляції — найкращим місцем для виявлення будь-якої проблеми.

Що дозволено, а що ні

✅ Дозволено

  • Порівняння двох значень одного enum class
  • Присвоєння значення того самого типу
  • Передача як аргумент функції відповідного типу
  • Використання у switch-операторі

❌ Заборонено

  • Порівняння значень різних enum class
  • Неявна конвертація в int
  • Неявна конвертація з int
  • Арифметичні операції безпосередньо над enum class

Порівняння всередині одного enum class — природно і очевидно:

Main.cpp
#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;
}
./Main
$ ./Main
Рожевий!
Execution finished with exit code 0.

Явна конвертація через static_cast

Оскільки неявна конвертація enum classint заборонена, спроба вивести значення через std::cout дасть помилку компіляції:

enum class Color { Pink, Gray };
Color c = Color::Gray;

std::cout << c; // ❌ помилка: no match for 'operator<<'

Якщо числове значення все ж потрібне, використовується явна конвертація (explicit cast) через static_cast:

Main.cpp
#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;
}
./Main
$ ./Main
1
Execution finished with exit code 0.

Зворотна конвертація: небезпека UB

Конвертація intenum 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>. Цей підхід автоматично визначає базовий тип перерахування:

Main.cpp
#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;
}
./Main
$ ./Main
Числове значення: 1
Execution finished with exit code 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 для прапорців).

Перевіримо розміри:

Main.cpp
#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;
}
./Main
$ ./Main
sizeof(StatusDefault) = 4 байт
sizeof(StatusSmall) = 1 байт
sizeof(StatusLarge) = 4 байт
Execution finished with exit code 0.

enum class у switch-операторі

Конструкція switch з enum class повністю аналогічна до звичайного enum, але у кожному case потрібна кваліфікація через :::

Main.cpp
#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;
}
./Main
$ ./Main
Поточний напрямок: Схід
Execution finished with exit code 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

Властивістьenumenum class
Область видимості енумераторівГлобальна (витік)Локальна (ізольована)
Неявна конвертація → int
Порівняння різних перерахувань✅ (баґ!)❌ (помилка компіляції)
Задання базового типуЧастково (C++11)✅ Повністю
Попередження про неповний switch
Конфлікти імен між перерахуваннямиМожливіНеможливі
Рекомендований у C++11+
Єдиний законний привід використовувати незахищений enum у сучасному C++ — взаємодія зі старим C-кодом або API, яке явно очікує нескваліфіковані константи. У всіх нових кодах слід надавати перевагу enum class.

Практика

Рівень 1 — Базовий

Рівень 2 — Логіка та системи

Рівень 3 — Архітектура


Резюме

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 вимагає повної кваліфікації; компілятор попереджає про непокриті гілки.
Для нового коду на C++11 і новіших стандартах: використовуйте enum class за замовчуванням. Повертайтесь до незахищеного enum лише тоді, коли цього явно вимагає сумісність із зовнішнім кодом.
Copyright © 2026