enum)Уявіть: ви відкриваєте чужий код — або навіть власний, написаний два тижні тому — і бачите функцію:
int readFile(const std::string& path) {
if (!openFile(path))
return -1;
if (!parseFile())
return -2;
if (!readContents())
return -3;
return 0;
}
Що означає -2? Помилка парсингу? Недостатньо прав? Диск заповнений? Без коментаря або документації це просто магічне число (magic number) — значення, яке несе змістовне навантаження, але само по собі нічого не пояснює. Щоб зрозуміти, що воно означає, доводиться шукати оригінальне визначення, яке може бути в іншому файлі, або ж вгадувати з контексту.
Магічні числа — це одна з найпоширеніших причин помилок і важкого супроводу коду. Програміст, який викликає readFile(), мусить пам'ятати, що -1 — відкрити не вдалося, -2 — парсинг провалився, а -3 — читання обірвалося. Ця інформація існує лише в голові, не в коді.
Стандартна відповідь мови C++ на цю проблему — перерахування (enumeration).
Перелічуваний тип даних (enumerated type, або скорочено — enumeration) — це тип, усі можливі значення якого визначені як іменовані цілочисельні константи. Кожна така константа називається енумератором (enumerator). Перерахування оголошується за допомогою ключового слова enum.
Найпростіший спосіб зрозуміти мотивацію — переписати попередній приклад через перерахування:
enum ParseResult {
SUCCESS = 0,
ERR_OPEN_FILE = -1,
ERR_PARSE = -2,
ERR_READ = -3,
};
ParseResult readFile(const std::string& path) {
if (!openFile(path))
return ERR_OPEN_FILE;
if (!parseFile())
return ERR_PARSE;
if (!readContents())
return ERR_READ;
return SUCCESS;
}
Тепер код «говорить» сам за себе. Якщо функція повернула ERR_PARSE, будь-який розробник розуміє: виникла проблема з синтаксичним аналізом файлу. Жодних коментарів не потрібно — значення закладено безпосередньо в ім'я.
Зверніть увагу на декілька деталей синтаксису:
enum, після якого йде ім'я типу.} — точно так само, як структура або клас.ERR_READ = -3,), що спрощує додавання нових значень без редагування попереднього рядка.ParseResult), а самі енумератори — повністю у верхньому регістрі (ERR_OPEN_FILE). Ця конвенція допомагає відразу відрізнити тип від змінної і константу від звичайного ідентифікатора.Загальна форма оголошення перерахування виглядає так:
enum ІмʼяТипу {
ЕНУМЕРАТОР_1,
ЕНУМЕРАТОР_2,
ЕНУМЕРАТОР_3,
};
Розглянемо конкретний приклад — перерахування напрямків руху:
enum Direction {
DIRECTION_NORTH,
DIRECTION_SOUTH,
DIRECTION_EAST,
DIRECTION_WEST,
};
Після оголошення типу можна визначати змінні цього типу:
Direction playerDir = DIRECTION_NORTH;
Direction enemyDir(DIRECTION_SOUTH); // альтернативний синтаксис ініціалізації
Важливо розрізняти оголошення типу і визначення змінної. Рядок enum Direction { ... }; лише повідомляє компілятору, що існує такий тип — пам'ять при цьому не виділяється. Пам'ять виділяється лише тоді, коли ви оголошуєте змінну цього типу (наприклад, Direction playerDir).
Direction оголошено у глобальному просторі, то DIRECTION_NORTH також є глобальною константою. Саме тому прийнято додавати префікс назви типу до кожного енумератора — щоб уникнути конфліктів імен.Кожному енумератору компілятор автоматично присвоює ціле число, починаючи з нуля для першого елемента. Кожний наступний елемент отримує значення, на одиницю більше за попередній. Розглянемо перерахування кольорів:
enum Color {
COLOR_RED, // 0
COLOR_GREEN, // 1
COLOR_BLUE, // 2
COLOR_YELLOW, // 3
};
Якщо вивести значення змінної цього типу через std::cout, ми побачимо ціле число — не ім'я:
#include <iostream>
int main() {
Color c = COLOR_BLUE;
std::cout << c << "\n"; // виведе: 2
return 0;
}
Це тому, що enum у C++ — це «тонка обгортка» навколо цілого числа. Значення енумератора неявно конвертується в int для виведення.
Ви можете вручну задати значення будь-якому енумератору. Усі наступні (без явного значення) продовжують відлік з останнього визначеного:
enum StatusCode {
SUCCESS = 0, // явно 0
ERR_NOT_FOUND = 404, // явно 404
ERR_SERVER = 500, // явно 500
ERR_UNKNOWN, // автоматично 501 (500 + 1)
};
Значення можуть бути від'ємними, що особливо корисно для кодів помилок:
enum ParseResult {
PARSE_OK = 0,
PARSE_ERR_EMPTY = -1,
PARSE_ERR_UTF8 = -2,
PARSE_ERR_EOF = -3,
};
switch, де обидва значення виявляться еквівалентними. Уникайте дублювання значень без вагомої причини.Стан змінних перерахування добре підходить для візуалізації у відлагоджувачеві. Розглянемо, як виглядатиме дебаг-панель для нашого прикладу:
| Name | Type | Value |
|---|---|---|
| ◢playerDir | Direction | DIRECTION_NORTH (0) |
| ◢enemyDir | Direction | DIRECTION_SOUTH (1) |
| ◢status | ParseResult | PARSE_OK (0) |
Зверніть увагу: хороший відлагоджувач показує і символьне ім'я (DIRECTION_NORTH), і числове значення (0) одночасно — це одна зі справжніх переваг перерахувань над голими числами у відлагодженні.
Перелічуваний тип відноситься до родини цілочисельних типів. Стандарт C++ не фіксує конкретний розмір: компілятор обирає його самостійно, щоби вмістити всі значення перерахування. На практиці, для більшості enum розмір збігається з sizeof(int) — зазвичай 4 байти.
Перевіримо це:
#include <iostream>
enum Direction { NORTH, SOUTH, EAST, WEST };
int main() {
std::cout << "sizeof(Direction) = " << sizeof(Direction) << " байт\n";
std::cout << "sizeof(int) = " << sizeof(int) << " байт\n";
return 0;
}
Подивимося на внутрішнє представлення в пам'яті. Перше значення DIRECTION_NORTH — це просто ціле число 0, записане у 4 байти:
Перші 4 байти (підсвічені) — це змінна playerDir = DIRECTION_NORTH, значення якої дорівнює 0. Наступні 4 байти — enemyDir = DIRECTION_SOUTH, значення 1. Як бачите, перерахування займає рівно стільки пам'яті, скільки int.
enum та intОскільки enum побудований поверх цілого числа, C++ дозволяє неявну конвертацію (implicit conversion) значення перерахування в int. Саме цим пояснюється, що std::cout << COLOR_BLUE виводить число 2, а не рядок "COLOR_BLUE".
Ту саму конвертацію можна виконати явно:
Color c = COLOR_GREEN;
int numericValue = c; // неявна конвертація: numericValue = 1
int explicit_val = static_cast<int>(c); // явна конвертація, теж 1
Обидва варіанти валідні. Явна версія через static_cast вважається кращою практикою — вона показує, що ви усвідомлено перетворюєте тип.
Зворотне перетворення — з int у enum — заборонено без явного приведення. Компілятор відмовить:
Color c = 2; // ❌ помилка компіляції: cannot convert 'int' to 'Color'
Якщо така конвертація все ж необхідна, використовуйте static_cast:
int input = 2;
Color c = static_cast<Color>(input); // ✅ явна конвертація
static_cast<Color>(999) формально є валідним кодом, але поведінка не визначена (undefined behavior), якщо значення 999 не є жодним з оголошених енумераторів. Це особливо небезпечно при отриманні введення від користувача або з мережі — завжди перевіряйте діапазон перш ніж кастити.std::cinКомпілятор не дозволяє читати значення перерахування безпосередньо через std::cin:
Color c;
std::cin >> c; // ❌ помилка компіляції
Правильний підхід — зчитати ціле число, перевірити його та конвертувати:
#include <iostream>
int main() {
int input;
std::cout << "Оберіть колір (0=червоний, 1=зелений, 2=синій): ";
std::cin >> input;
if (input < 0 || input > 2) {
std::cout << "Невідомий колір!\n";
return 1;
}
Color c = static_cast<Color>(input);
std::cout << "Ви обрали COLOR_" << c << "\n"; // виведе числове значення
return 0;
}
Коли ми пишемо std::cout << COLOR_BLUE, бачимо 2 замість "Blue". Це поведінка за замовчуванням, яка нас рідко влаштовує. Щоб вивести зрозуміле текстове ім'я, потрібно написати окрему функцію. Найпоширеніший підхід — switch-вираз:
#include <iostream>
enum Color {
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE,
};
const char* colorName(Color c) {
switch (c) {
case COLOR_RED: return "Червоний";
case COLOR_GREEN: return "Зелений";
case COLOR_BLUE: return "Синій";
default: return "Невідомий";
}
}
int main() {
Color c = COLOR_GREEN;
std::cout << "Колір: " << colorName(c) << "\n";
return 0;
}
Розберемо ключові частини:
colorName приймає значення типу Color і повертає рядок const char* — текстовий опис.switch (c) порівнює значення з кожним case. Коли C++ виконує switch над значенням перерахування, він фактично порівнює цілі числа — але ваш код при цьому залишається читабельним.default є обов'язковою, якщо ви хочете захиститися від ситуацій, коли в змінну типу Color потрапило непередбачене значення через небезпечний static_cast.<< для типів перерахувань. Але для C++14/17, де більшість навчальних програм ще працює, власна функція-конвертер — це найчистіше рішення.Тепер поговоримо про один з найважливіших обмежень незахищених (unscoped) перерахувань. Усі енумератори потрапляють у той самий простір імен, що і навколишній код. Це означає, що два різних enum не можуть мати енумераторів з однаковими іменами в одному файлі:
enum Fruit {
YELLOW, // значення 0
RED, // значення 1
GREEN, // значення 2
};
enum TrafficLight {
RED, // ❌ помилка! RED вже оголошено в Fruit
YELLOW, // ❌ помилка! YELLOW вже оголошено в Fruit
GREEN, // ❌ помилка!
};
Компілятор видасть помилку «redefinition of enumerator» для кожного конфліктуючого імені.
Класичний обхідний шлях — додавати до кожного енумератора префікс з назви перерахування:
enum Fruit {
FRUIT_YELLOW,
FRUIT_RED,
FRUIT_GREEN,
};
enum TrafficLight {
TRAFFIC_RED,
TRAFFIC_YELLOW,
TRAFFIC_GREEN,
};
Тепер конфліктів немає — кожне ім'я унікальне. Але погодьтеся: TRAFFIC_RED і FRUIT_YELLOW — це досить громіздко. І все одно: якщо хтось назве ще один enum з префіксом FRUIT_, конфлікт повернеться.
Це фундаментальна вада незахищених перерахувань. У наступній статті «Класи-перерахування (enum class)» ми розглянемо елегантне рішення цієї проблеми, введене у стандарті C++11.
int дозволяє порівнювати значення різних перерахувань між собою без жодного попередження від компілятора. Наприклад, FRUIT_YELLOW == TRAFFIC_RED (обидва мають значення 0) компілюється без помилок і повертає true — явна помилка, яка непомітно проникає в логіку програми. Це ще одна причина надавати перевагу enum class.enumНайпоширеніший і найцінніший застосунок перерахувань — замінити числові коди помилок на іменовані константи. Ми вже бачили це у вступі, розглянемо повніший приклад з обробкою на боці викликача:
#include <iostream>
#include <string>
enum FileResult {
FILE_OK = 0,
FILE_ERR_NOT_FOUND = -1,
FILE_ERR_PERM = -2,
FILE_ERR_CORRUPT = -3,
};
FileResult loadConfig(const std::string& path) {
// Симуляція: у реальному коді тут відкриваємо файл
if (path.empty())
return FILE_ERR_NOT_FOUND;
if (path == "/root/secret.cfg")
return FILE_ERR_PERM;
return FILE_OK;
}
int main() {
FileResult result = loadConfig("settings.cfg");
switch (result) {
case FILE_OK:
std::cout << "Конфігурацію завантажено успішно.\n";
break;
case FILE_ERR_NOT_FOUND:
std::cout << "Помилка: файл не знайдено.\n";
break;
case FILE_ERR_PERM:
std::cout << "Помилка: недостатньо прав доступу.\n";
break;
case FILE_ERR_CORRUPT:
std::cout << "Помилка: файл пошкоджений.\n";
break;
}
return 0;
}
Зверніть увагу на підхід до обробки: замість порівняння result == -1 ми порівнюємо result == FILE_ERR_NOT_FOUND. Читач коду одразу розуміє семантику, не заглядаючи в документацію.
Кінцевий автомат (finite state machine, FSM) — це поведінкова модель, де система може перебувати в одному з кінцевого набору станів і переходить між ними за певними правилами. Перерахування — ідеальний засіб для опису таких станів збереження стану.
Уявімо простий ігровий персонаж, який може стояти, ходити або бігти:
#include <iostream>
enum PlayerState {
PLAYER_STATE_IDLE, // стоїть
PLAYER_STATE_WALKING, // іде
PLAYER_STATE_RUNNING, // біжить
PLAYER_STATE_DEAD, // загинув
};
const char* stateName(PlayerState state) {
switch (state) {
case PLAYER_STATE_IDLE: return "Стоїть";
case PLAYER_STATE_WALKING: return "Іде";
case PLAYER_STATE_RUNNING: return "Біжить";
case PLAYER_STATE_DEAD: return "Загинув";
default: return "Невідомий стан";
}
}
void handleInput(PlayerState& state, char key) {
switch (state) {
case PLAYER_STATE_IDLE:
if (key == 'w') state = PLAYER_STATE_WALKING;
break;
case PLAYER_STATE_WALKING:
if (key == 'r') state = PLAYER_STATE_RUNNING;
if (key == 's') state = PLAYER_STATE_IDLE;
break;
case PLAYER_STATE_RUNNING:
if (key == 's') state = PLAYER_STATE_IDLE;
break;
case PLAYER_STATE_DEAD:
// мертвий персонаж не реагує на введення
break;
}
}
int main() {
PlayerState state = PLAYER_STATE_IDLE;
std::cout << "Стан гравця: " << stateName(state) << "\n";
handleInput(state, 'w');
std::cout << "Після натиснення W: " << stateName(state) << "\n";
handleInput(state, 'r');
std::cout << "Після натиснення R: " << stateName(state) << "\n";
handleInput(state, 's');
std::cout << "Після натиснення S: " << stateName(state) << "\n";
return 0;
}
Важливі моменти у структурі цього прикладу:
handleInput приймає PlayerState& — посилання на стан, щоб мати можливість змінювати його всередині функції. Відповідно до вивченого в «Посиланнях», саме посилання (а не копія) дає нам змогу модифікувати оригінальну змінну.case у switch відповідає за всі переходи з відповідного стану. Додавання нового стану вимагає лише додавання нового case — структура коду залишається чіткою.PLAYER_STATE_DEAD є термінальним: персонаж у ньому не реагує на введення з клавіатури.Перерахування ідеально підходять для будь-якого набору взаємовиключних значень — напрямків у просторі, режимів роботи пристрою, сторін компаса:
enum CompassDirection {
COMPASS_NORTH,
COMPASS_NORTHEAST,
COMPASS_EAST,
COMPASS_SOUTHEAST,
COMPASS_SOUTH,
COMPASS_SOUTHWEST,
COMPASS_WEST,
COMPASS_NORTHWEST,
COMPASS_COUNT, // корисний «сторожовий» елемент: завжди == кількості напрямків
};
enum SortMode {
SORT_ASCENDING, // від меншого до більшого
SORT_DESCENDING, // від більшого до меншого
SORT_SHUFFLE, // випадковий порядок
};
void sortArray(int* arr, int size, SortMode mode) {
// Залежно від режиму — різна логіка сортування
if (mode == SORT_ASCENDING) {
// ... сортування за зростанням
} else if (mode == SORT_DESCENDING) {
// ... сортування за спаданням
}
}
Зверніть увагу на трюк COMPASS_COUNT — технічно це просто наступне ціле число після останнього реального напрямку (= 8), але воно дозволяє легко отримати кількість елементів у перерахуванні без ручного підрахунку або магічних чисел.
enum: огляд📌 Оголошення та синтаксис
enum, ім'я типу, список енумераторів;🔢 Значення та нумерація
int)⚠️ Обмеження unscoped enum
enum → int дозволено неявноint → enum лише через static_cast✅ Найкращі застосування
Оголосіть перерахування Direction з чотирма значеннями: NORTH, SOUTH, EAST, WEST (з префіксом DIRECTION_). Виведіть числові значення всіх чотирьох енумераторів у циклі.
Підказка: Ви не можете ітеруватися по enum напряму. Скористайтеся трюком DIRECTION_COUNT або виводьте кожне значення вручну.
Дана функція:
int checkAge(int age) {
if (age < 0) return -1;
if (age < 18) return -2;
if (age > 120) return -3;
return 0;
}
Замініть числові коди повернення (-1, -2, -3, 0) на перерахування AgeStatus з відповідними іменованими значеннями. Напишіть функцію printAgeStatus(AgeStatus), яка виводить текстовий опис.
ParseResult зі значеннями: PARSE_SUCCESS, PARSE_ERR_EMPTY, PARSE_ERR_INVALID_CHAR, PARSE_ERR_OVERFLOW. Напишіть функцію std::string getStatusMessage(ParseResult), яка повертає детальне текстове повідомлення для кожного статусу. Продемонструйте роботу: обходьте всі значення від 0 до PARSE_COUNT - 1, кастуйте через static_cast та виводьте відповідне повідомлення.LIFT_IDLE (зупинений), LIFT_MOVING_UP (рухається вгору), LIFT_MOVING_DOWN (рухається вниз), LIFT_DOOR_OPEN (двері відкриті). Напишіть функцію transitLift(LiftState& state, char command), де команди: 'u' — рухатися вгору, 'd' — вниз, 'o' — відкрити двері, 'c' — закрити. Враховуйте: двері не можна відкрити під час руху; після закриття дверей стан переходить у IDLE.Реалізуйте перерахування HttpStatus з значеннями для кодів: 200 (OK), 201 (Created), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 500 (Internal Server Error), 503 (Service Unavailable). Явно задайте числові значення. Напишіть три функції:
const char* httpStatusText(HttpStatus) — повертає текстовий опис.bool isSuccess(HttpStatus) — true для 2xx кодів.bool isClientError(HttpStatus) — true для 4xx кодів.bool isServerError(HttpStatus) — true для 5xx кодів.Напишіть main(), де ви симулюєте кілька відповідей сервера і для кожної виводите: код, текст, категорію.
Перерахування (enum) — це один із фундаментальних інструментів для написання читабельного та семантично точного коду на C++. Замість того щоб оперувати безіменними числами, ми даємо стану програми конкретну назву: FILE_ERR_NOT_FOUND замість -1, PLAYER_STATE_RUNNING замість 2.
Ключові висновки цього розділу:
enum оголошує новий тип із заздалегідь визначеним набором іменованих цілочисельних констант — енумераторів.0, але будь-яке значення може бути задано явно (включно з від'ємними).enum — unscoped: всі його енумератори потрапляють у простір імен навколишнього коду, що може призводити до конфліктів.enum → int відбувається неявно; int → enum вимагає static_cast і несе ризик невизначеної поведінки.enum class — захищене перерахування з власним простором імен і суворою типізацією.enum class)» — ми побачимо, як enum class вирішує всі проблеми, описані в цьому розділі, і чому у сучасному C++ він є стандартним вибором.