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
часто да нет
Меньше конфликтов имён нет да
“По умолчанию” в 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. И дальше программа начинает вести себя странно, потому что вы сами обошли типовую защиту.

1
Задача
C++ SELF, 19 уровень, 1 лекция
Недоступна
Приветствие дня
Приветствие дня
1
Задача
C++ SELF, 19 уровень, 1 лекция
Недоступна
Команда светофора
Команда светофора
1
Задача
C++ SELF, 19 уровень, 1 лекция
Недоступна
Заявка поддержки
Заявка поддержки
1
Задача
C++ SELF, 19 уровень, 1 лекция
Недоступна
Состояние плеера
Состояние плеера
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ