Перечисления на C++ — это универсальный инструмент для создания именованных констант, но его истинный потенциал проявляется при эффективном прохождении перечислений.
При создании игрового движка, надежного конечного автомата или системы конфигурирования, благодаря умению перебирать значения перечислимого типа, значительно повышаются гибкость и удобство восприятия кода.
Сложность прохождения перечислений
На первый взгляд прохождение перечислений кажется простым.
Но для этой задачи на C++ нет встроенного механизма. Перечисления здесь считаются отдельными типами, а не легко проходимыми коллекциями или диапазонами.
Из-за этого ограничения появились креативные решения разработчиков, каждое со своими нюансами и сценариями.
Познакомимся с практическими подходами к прохождению перечислений, дополним их примерами кода и реальными сценариями.
Массив с циклом for на основе диапазона
Это простейший способ пройтись по значениям перечислимого типа, особенно когда нужно выполнять операции с каждым таким значением последовательно.
#include <iostream>
#include <array>
enum class Color { Red, Green, Blue, Yellow };
int main() {
std::array<Color, 4> colors = {Color::Red, Color::Green, Color::Blue, Color::Yellow};
for (const auto& color : colors) {
switch (color) {
case Color::Red: std::cout << "Red\n"; break;
case Color::Green: std::cout << "Green\n"; break;
case Color::Blue: std::cout << "Blue\n"; break;
case Color::Yellow: std::cout << "Yellow\n"; break;
}
}
return 0;
}
Этом простому методу не требуются дополнительные библиотеки.
Но у него имеется существенный недостаток: при каждом добавлении или удалении значений перечислимого типа массив нужно обновлять вручную. Это чревато проблемами сопровождения в крупных проектах.
Магия шаблона: автоматическое прохождение перечисления
Проблема сопровождения из предыдущего подхода решается метапрограммированием на основе шаблонов, при котором автоматически генерируется диапазон значений перечислимого типа. Этот прием опирается на функционал C++17 — std::underlying_type и constexpr if.
#include <iostream>
#include <type_traits>
enum class Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday };
template <typename E>
constexpr auto to_underlying(E e) noexcept {
return static_cast<std::underlying_type_t<E>>(e);
}
template <typename E>
class EnumIterator {
int value;
public:
explicit EnumIterator(int v) : value(v) {}
E operator*() const { return static_cast<E>(value); }
EnumIterator& operator++() { ++value; return *this; }
bool operator!=(const EnumIterator& other) const { return value != other.value; }
};
template <typename E>
class EnumRange {
int begin_value, end_value;
public:
EnumRange(E begin, E end) : begin_value(to_underlying(begin)), end_value(to_underlying(end)) {}
EnumIterator<E> begin() const { return EnumIterator<E>(begin_value); }
EnumIterator<E> end() const { return EnumIterator<E>(end_value + 1); }
};
template <typename E>
EnumRange<E> enum_range(E begin, E end) {
return EnumRange<E>(begin, end);
}
int main() {
for (auto day : enum_range(Weekday::Monday, Weekday::Sunday)) {
switch (day) {
case Weekday::Monday: std::cout << "Monday\n"; break;
case Weekday::Tuesday: std::cout << "Tuesday\n"; break;
case Weekday::Wednesday: std::cout << "Wednesday\n"; break;
case Weekday::Thursday: std::cout << "Thursday\n"; break;
case Weekday::Friday: std::cout << "Friday\n"; break;
case Weekday::Saturday: std::cout << "Saturday\n"; break;
case Weekday::Sunday: std::cout << "Sunday\n"; break;
}
}
return 0;
}
Такой подход более гибкий и сопровождаемый. Значения перечислимого типа добавляются или удаляются без изменения кода. Однако предполагается, что значения перечислимого типа непрерывны и начинаются с нуля. Если эти критерии перечислением не соблюдаются, реализацию придется корректировать.
Рефлексия-подобное поведение с макросами
Встроенной поддержки рефлексии на C++ нет, а аналогичная функциональность обеспечивается макросами. Благодаря этому методу строковые представления связываются со значениями перечислимого типа и они легко проходятся.
#include <iostream>
#include <string_view>
#include <array>
#define ENUM_DEFINE(enumName, ...) \
enum class enumName { __VA_ARGS__ }; \
static constexpr std::array enumName##_strings = { \
#__VA_ARGS__ \
}; \
static constexpr std::array<enumName, enumName##_strings.size()> \
enumName##_values = { \
__VA_ARGS__ \
}; \
static constexpr std::string_view enumName##_to_string(enumName value) { \
return enumName##_strings[static_cast<size_t>(value)]; \
}
ENUM_DEFINE(Planet, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune)
int main() {
for (const auto& planet : Planet_values) {
std::cout << "Planet: " << Planet_to_string(planet) << '\n';
}
return 0;
}
Вот преимущества этого подхода, основанного на макросах:
- Для значений перечислимого типа им автоматически генерируются строковые представления.
- Им создается массив всех значений перечислимого типа, отчего прохождение упрощается.
- Значения перечислимого типа легко добавляются или удаляются без нарушения итеративной логики.
Недостаток подхода — использование макросов, некоторые разработчики предпочитают их избегать из-за потенциальных проблем с отладкой и ясностью кода.
Реальное применение: конечный автомат для автомата торгового
Применим методы прохождения перечислений к реальному сценарию.
В этом примере демонстрируется, как прохождением перечисления сложная логика упрощается и код становится более сопровождаемым.
#include <iostream>
#include <string_view>
#include <array>
#include <functional>
#define ENUM_DEFINE(enumName, ...) \
enum class enumName { __VA_ARGS__ }; \
static constexpr std::array enumName##_strings = { \
#__VA_ARGS__ \
}; \
static constexpr std::array<enumName, enumName##_strings.size()> \
enumName##_values = { \
__VA_ARGS__ \
}; \
static constexpr std::string_view enumName##_to_string(enumName value) { \
return enumName##_strings[static_cast<size_t>(value)]; \
}
ENUM_DEFINE(VendingMachineState, Idle, WaitingForMoney, SelectingProduct, DispensingProduct, Refunding)
class VendingMachine {
private:
VendingMachineState current_state = VendingMachineState::Idle;
float inserted_money = 0.0f;
void transition_to(VendingMachineState new_state) {
std::cout << "Transitioning from " << VendingMachineState_to_string(current_state)
<< " to " << VendingMachineState_to_string(new_state) << '\n';
current_state = new_state;
}
public:
void insert_money(float amount) {
if (current_state == VendingMachineState::Idle || current_state == VendingMachineState::WaitingForMoney) {
inserted_money += amount;
std::cout << "Inserted $" << amount << ". Total: $" << inserted_money << '\n';
transition_to(VendingMachineState::WaitingForMoney);
} else {
std::cout << "Cannot insert money in the current state.\n";
}
}
void select_product(float price) {
if (current_state == VendingMachineState::WaitingForMoney) {
if (inserted_money >= price) {
transition_to(VendingMachineState::DispensingProduct);
std::cout << "Dispensing product. Enjoy!\n";
float change = inserted_money - price;
if (change > 0) {
std::cout << "Returning change: $" << change << '\n';
}
inserted_money = 0;
transition_to(VendingMachineState::Idle);
} else {
std::cout << "Insufficient funds. Please insert more money.\n";
}
} else {
std::cout << "Cannot select product in the current state.\n";
}
}
void cancel_transaction() {
if (current_state != VendingMachineState::Idle) {
transition_to(VendingMachineState::Refunding);
std::cout << "Cancelling transaction. Refunding $" << inserted_money << '\n';
inserted_money = 0;
transition_to(VendingMachineState::Idle);
} else {
std::cout << "No active transaction to cancel.\n";
}
}
void run_diagnostics() {
std::cout << "Running diagnostics...\n";
for (const auto& state : VendingMachineState_values) {
std::cout << "Checking state: " << VendingMachineState_to_string(state) << '\n';
// Моделируется проверка каждого состояния
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "Diagnostics complete. All states verified.\n";
}
};
int main() {
VendingMachine vm;
vm.insert_money(1.0f);
vm.insert_money(0.5f);
vm.select_product(1.25f);
vm.insert_money(2.0f);
vm.select_product(1.75f);
vm.cancel_transaction();
vm.run_diagnostics();
return 0;
}
Здесь прохождение перечисления используется двумя ключевыми способами:
- Макросом ENUM_DEFINE генерируются вспомогательные функции и массивы, которыми упрощается работа с перечислением
VendingMachineState. - Функцией
run_diagnosticsпроходятся все возможные состояния, так прохождение перечисления используется для всестороннего тестирования или получения отчетов.
В этом подходе конечный автомат легко расширить. Для добавления нового состояния, например OutOfOrder, просто обновляем строку ENUM_DEFINE. Остальной код, включая диагностику, автоматически адаптируется к новому состоянию.
Заключение
Прохождение перечислений на C++ не такое простое, как в других языках. Но благодаря изученным нами методам оно становится мощным инструментом программирования.
От простых подходов с массивами и метапрограммирования на основе шаблонов до решений с макросами — каждому методу находится свое место, исходя из конкретных задач и ограничений.
Читайте также:
- Как проходится ассоциативный массив на C++
- C++: руководство по считыванию CSV-файлов
- C++: подробное руководство по разыменованию указателя
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: C++ Enum Iteration: Comprehensive Guide





