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
(нібито «todo», але це вже домовленість «між рядками»).inProgress = false
Що більше статусів, то більше прапорців, а отже — більше «нелегальних комбінацій».
«Зробімо 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.
Невелика підсумкова таблиця:
| Як зберігати статус | Приклад | Проблема |
|---|---|---|
|
|
лише 2 стани; далі починаються обхідні рішення |
|
|
магічні числа; легко отримати «випадкове 42» |
|
|
описки створюють нові «значення» |
|
|
типобезпечно і самодокументовано |
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++ є два види перелічень:
- enum — unscoped (старий стиль)
- enum class — scoped (сучасний стиль, зазвичай кращий вибір)
Проблема звичайного 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)
- не дає неявно перетворювати себе на число або булеве значення
Невелика шпаргалка:
| Властивість | |
|
|---|---|---|
| Імена значень | у поточній області видимості | усередині типу () |
Неявно перетворюється на |
часто так | ні |
| Менше конфліктів імен | ні | так |
| «За замовчуванням» у сучасному 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. І далі програма починає поводитися дивно, бо ви самі обійшли типовий захист.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ