JavaRush /Курси /C++ SELF /Закритий набір значень: enum і enum class

Закритий набір значень: enum і enum class

C++ SELF
Рівень 19 , Лекція 1
Відкрита

1. Вступ

Учорашня модель була дуже зручною:

struct Task {
    int id;							// id завдання
    std::string title;	// назва завдання
    bool done = false;  // статус: виконано/не виконано
};

Проблема виникає щойно ви хочете додати третій статус: «у процесі».

І тут bool уже не справляється, бо він уміє сказати лише «так/ні», а нам потрібно передати три різні стани.

Найтиповіше «швидке рішення» новачка — додати другий прапорець:

struct Task {
    int id;
    std::string title;
    bool done = false;
    bool inProgress = false; // а чи можуть обидва бути true? а обидва false?
};

Код компілюється. А от логіка — вже ні. Тепер можливі такі стани:

  • done = true
    і
    inProgress = true
    (завдання одночасно виконане й досі виконується… Квантова механіка, не інакше),
  • done = false
    і
    inProgress = false
    (нібито «todo», але це вже домовленість «між рядками»).

Що більше статусів, то більше прапорців, а отже — більше «нелегальних комбінацій».

«Зробімо int status» — на сцену виходять магічні числа

Наступна ідея: «ну, я просто все закодую числами».

int status = 0; // 0=todo, 1=in_progress, 2=done (десь у голові)
status = 42;    // компілятор не проти, але логіка вже плаче

Проблема тут не в int як типі. Проблема в тому, що тип ніяк не відображає сенсу. Якщо за тиждень ви побачите if (status == 2), це вже не код, а ребус.

«Зробімо std::string status» — описка стає новим статусом

Якщо перейти до рядків, краще не стане:

std::string status = "done";
status = "inprogres"; // описка -> “новий статус”, про який ніхто не домовлявся

Рядки часто призводять до зоопарку варіантів: "Done", "done", "DONE", "in progress", "in_progress"

Що нам насправді потрібно

Нам потрібен тип, який прямо каже: значення може бути лише одним із заздалегідь відомих варіантів. Це і є перелічення: enum і enum class. enum — старіший варіант, його вже майже не використовують. Тому ми відразу вивчатимемо сучасний enum class.

Невелика підсумкова таблиця:

Як зберігати статус Приклад Проблема
bool
done = true/false
лише 2 стани; далі починаються обхідні рішення
int
status = 2
магічні числа; легко отримати «випадкове 42»
std::string
"in_progress"
описки створюють нові «значення»
enum class
Status::InProgress
типобезпечно і самодокументовано

2. enum class: базовий синтаксис і робота зі значеннями

Оголошення

За допомогою enum class можна створити новий тип, перелічивши всі його можливі значення:

enum class Status {
    Todo,
    InProgress,
    Done
}; // так, крапка з комою потрібна

Тут:

  • Statusназва типу
  • Todo, InProgress, Doneелементи перелічення (можливі значення)
  • ; після } обов’язкова (як і в struct)

Створення, присвоєння, порівняння

Перелічення — це звичайний тип: його можна зберігати у змінній, присвоювати й порівнювати.

enum class Status { Todo, InProgress, Done };

void Demo() {
    Status s = Status::Todo;        // створили
    s = Status::InProgress;         // присвоїли

    if (s == Status::InProgress) {  // порівняли
        // робимо щось
    }
}

Звичка, яку варто запам’ятати: у enum class значення записують як Status::Todo, а не просто Todo. Це не «зайвий шум», а захист від конфліктів імен.

Важливий нюанс: enum class не перетворюється на int і bool автоматично

Старий тип enum міг неявно перетворюватися на int і навіть брати участь у перевірках як «істина/хиба».

enum Status { Todo, InProgress, Done };

void Bad() {
    Status s = Done;
    int x = s;
    if (s) { }
}

Дуже зручно — і дуже… проблемно. У новому enum class від цього відмовилися. Тобто компілятор не дозволить вам випадково написати «статус як число» або «статус як істину/хибу».

3. Додаємо enum class у нашу модель Task

Перепишімо Task: замість bool done зробимо повноцінний статус.

#include <string>

enum class Status { Todo, InProgress, Done };

struct Task {
    int id;
    std::string title;
    Status status = Status::Todo; // статус за замовчуванням
};

Тепер створення завдань стає значно промовистішим:

Task a{1, "Buy milk"};                     // status = Todo (за замовчуванням)
Task b{2, "Pay rent", Status::InProgress}; // задано явно

І ще одна приємна деталь: неможливо випадково присвоїти щось не те.

Task t{1, "Fix bug"};
// t.status = 2; // помилка компіляції: і це саме те, що нам треба

4. Статуси й пріоритети: ще один enum class

Раз уже ми навчилися виражати «закритий набір значень», додаймо ще один такий набір: пріоритет.

enum class Priority {
    Low,
    Normal,
    High
};

І розширимо модель Task. Гарна новина: ми можемо додати поле так, щоб старий код майже не зламався, — достатньо задати значення за замовчуванням.

#include <string>

enum class Status   { Todo, InProgress, Done };
enum class Priority { Low, Normal, High };

struct Task {
    int id;
    std::string title;
    Status status = Status::Todo;
    Priority priority = Priority::Normal;
};

Тепер ми можемо писати так:

Task a{1, "Buy milk"}; // Todo + Normal
Task b{2, "Pay rent", Status::InProgress}; // InProgress + Normal
Task c{3, "Fix prod", Status::Todo, Priority::High}; // Todo + High

І ось де типобезпека стає майже «відчутною»:

Task t{1, "Write report"};

t.status = Priority::High; // помилка: різні типи!
t.priority = Status::Done; // теж помилка

Якби обидва поля були int, такі помилки були б цілком реальними й дуже неприємними.

5. Що краще: enum vs enum class

У C++ є два види перелічень:

  • enumunscoped (старий стиль)
  • enum classscoped (сучасний стиль, зазвичай кращий вибір)

Проблема звичайного enum: імена «витікають» назовні

enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // конфлікт імен Red/Green

У великому проєкті це легко перетворюється на постійне «а чому в мене Red уже зайнято?».

Проблема звичайного enum: надто легко перетворюється на int

enum Color { Red, Green, Blue };

void Surprise() {
    int x = Red;      // компілюється
    if (Green == 1) { // теж компілюється (і це сумнівно)
        // ...
    }
}

Тобто enum може «витікати» в числа, і ви знову опиняєтеся поруч із магічними значеннями.

Що дає enum class

Переваги enum class:

  • тримає імена всередині типу (наприклад, Status::Done)
  • не дає неявно перетворювати себе на число або булеве значення

Невелика шпаргалка:

Властивість
enum
enum class
Імена значень у поточній області видимості усередині типу (
Status::Done
)
Неявно перетворюється на
int
часто так ні
Менше конфліктів імен ні так
«За замовчуванням» у сучасному C++ скоріше ні так

Практичне правило курсу: якщо не впевнені — обирайте enum class.

6. Числові значення елементів перелічення

Значення «за замовчуванням»

Якщо ви не задаєте числа явно, елементи зазвичай мають значення 0, 1, 2… — за порядком.

enum class Status { Todo, InProgress, Done };
// зазвичай Todo=0, InProgress=1, Done=2

Але важливий момент: це лише деталь внутрішнього подання, а не сенс. Сенс записують так:

if (t.status == Status::Done) {
    // завдання виконано
}

а не так:

if (code == 2) { ... } // це повертає нас до магічних чисел

Явні коди: коли це справді потрібно

Іноді вам потрібні стабільні числа, наприклад у форматі файлу, протоколі або під час інтеграції з чужим API. Тоді задаємо значення явно:

enum class Status {
    Todo = 10,
    InProgress = 20,
    Done = 30
};

Якщо ви задаєте значення одному елементу, наступний елемент без значення продовжить «лічильник»:

enum class Status {
    Todo = 10,
    InProgress, // 11
    Done        // 12
};

Це зручно, але тут потрібна дисципліна: якщо вже вводите числа, варто чітко задокументувати, навіщо саме.

7. Underlying type: який тип насправді зберігається в enum

Перелічення зберігається в пам’яті як ціле число. Тип цього числа називається underlying type (базовий тип перелічення). Зазвичай це щось на кшталт int, але інколи хочеться вказати тип явно.

Найпоширеніші причини:

  • сумісність із зовнішнім форматом (наприклад, «у файлі статус зберігається як 1 байт»)
  • економія пам’яті у великих масивах і таблицях
  • бажання чітко обмежити діапазон

Синтаксис: : std::uint8_t

Ось як можна замінити внутрішній int на std::uint8_t:

#include <cstdint>

enum class Status : std::uint8_t {
    Todo,
    InProgress,
    Done
};

Тут є два практичні нюанси.

По-перше, усі значення мають вміщатися в обраний тип. По-друге, навіть якщо Status став 1-байтовим, sizeof(Task) може не зменшитися через вирівнювання (padding). Це не баг, а звичайна «геометрія пам’яті».

Якщо хочеться поекспериментувати і заодно побачити, що компілятор — живий організм, а не калькулятор, можна вивести розміри:

#include <iostream>

int main() {
    std::cout << sizeof(Status) << '\n';
    std::cout << sizeof(Task) << '\n';
}

Результати можуть відрізнятися залежно від компілятора й платформи — і це нормально.

8. Як отримати числове значення enum

std::to_underlying — правильний «офіційний» спосіб

У C++23 є функція std::to_underlying, яка повертає underlying-значення перелічення.

#include <iostream>
#include <utility> // std::to_underlying

enum class Status { Todo, InProgress, Done }; // 0, 1, 2

int main() {
    Status s = Status::Done;
    std::cout << std::to_underlying(s) << '\n'; // 2
}

Це корисно для налагодження або для передавання в протокол чи файл, якщо ви так домовилися.

Пастка: std::uint8_t у cout може друкуватися як символ

Якщо underlying type — std::uint8_t, то під час виведення в потік ви можете побачити «літеру», а не число. Бо uint8_t часто поводиться як unsigned char.

#include <cstdint>
#include <iostream>
#include <utility>

enum class Status : std::uint8_t { Todo, InProgress, Done };

int main() {
    auto code = std::to_underlying(Status::Done);
    std::cout << static_cast<int>(code) << '\n'; // друкуємо як число
}

Правило просте: якщо underlying тип «байтовий», для друку приводьте значення до int.

Якщо std::to_underlying недоступний: static_cast

Іноді середовище «застрягло» на старому стандарті або C++23 просто не ввімкнено. Тоді рятує явне приведення:

enum class Status { Todo, InProgress, Done };

int code = static_cast<int>(Status::Done); //  2

Сенс той самий: ви явно кажете компілятору: «так, я свідомо хочу число».

9. Небезпечна зона: перетворення з числа в enum

Насправді static_cast<Status>(42) — це потенційно небезпечна річ. Такий код компілюється:

enum class Status { Todo, InProgress, Done };

Status s = static_cast<Status>(42); // формально ок, логічно — сміття

Ви щойно створили значення Status, яке не має імені в переліченні. Це руйнує саму ідею «закритого набору значень».

enum class захищає від випадкових помилок, але якщо ви «ломом відчиняєте двері» через static_cast, компілятор уже не зобов’язаний вас рятувати.

Тонкість із Status s{};

Value-ініціалізація ({}) дає нульове underlying-значення.

enum class Status { Todo, InProgress, Done };

Status ok{}; //  underlying=0 ->  це Todo

Але якщо ви задали коди не з нуля:

enum class Status { Todo = 10, InProgress = 20, Done = 30 };

Status bad{}; // underlying=0 -> це НЕ Todo, це “неназване” значення

Практичний висновок: якщо вам потрібен статус за замовчуванням, задавайте його явно. У моделі ми саме так і робимо:

Status status = Status::Todo;

Якщо все ж доводиться отримувати enum із числа — перевіряйте значення

Поки ми ще не вивчаємо std::optional і «правильні результати парсингу» (це буде пізніше), сам принцип можна показати простіше: перевіряти числа перед присвоюванням.

enum class Status { Todo, InProgress, Done };

bool TryParseStatus(int code, Status& out) {
    if (code == 0) { out = Status::Todo; return true; }
    if (code == 1) { out = Status::InProgress; return true; }
    if (code == 2) { out = Status::Done; return true; }
    return false; // невідомий код
}

Так, тут знову з’являються числа, але тепер вони живуть в одному місці й використовуються лише як вхідний формат, а не як основа логіки в усій програмі.

10. using enum: як менше писати Status::...

C++20 додав using enum, який дає змогу зробити елементи перелічення доступними в поточній області видимості.

enum class Status { Todo, InProgress, Done };

void Demo() {
    using enum Status;   // Todo/InProgress/Done доступні без Status::
    Status s = Todo;
    s = InProgress;
}

Це зручно, але є просте правило доброго тону: використовуйте using enum локально, усередині функції. Якщо зробити так у глобальній області, легко отримати конфлікти й запитання на кшталт «хто такий Todo і звідки він узявся?».

11. Типові помилки під час роботи з enum / enum class

Помилка № 1: забули ; після оголошення перелічення.
Симптоми зазвичай виглядають як «expected ‘;’ …» або як дивні помилки в наступних рядках. Виправляється просто: enum class {...}; — завжди з крапкою з комою.

Помилка № 2: пишуть Todo замість Status::Todo.
У enum class значення містяться всередині типу. Це спеціально зроблено, щоб не було конфліктів і щоб код читався однозначно. Якщо дуже хочеться коротшого запису, використовуйте using enum Status; локально всередині функції.

Помилка № 3: намагаються використовувати enum class як int або bool.
enum class не перетворюється на числа чи логічні значення автоматично. Це не примха мови, а захист від беззмістовних перевірок на кшталт if (status) або присвоювань на кшталт status = 2;.

Помилка № 4: будують бізнес-логіку на числах 0/1/2.
Якщо ви десь бачите if (code == 2), значить enum «не зробив своєї роботи», і ви знову повернулися до магічних чисел. Правильний код має порівнювати Status::Done, а не 2.

Помилка № 5: роблять static_cast<Status>(число) без перевірки.
Так можна отримати «неназваний» статус, який не дорівнює ні Todo, ні InProgress, ні Done. І далі програма починає поводитися дивно, бо ви самі обійшли типовий захист.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ