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)
- не даёт неявно превращать себя в число/булево
Небольшая шпаргалка:
| Свойство | |
|
|---|---|---|
| Имена значений | в текущей области видимости | внутри типа () |
Неявно превращается в |
часто да | нет |
| Меньше конфликтов имён | нет | да |
| “По умолчанию” в modern 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. И дальше программа начинает вести себя странно, потому что вы сами обошли типовую защиту.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ