Теги: Си перечисление, enum.
В си выделен отдельный тип перечисление (enum), задающий набор всех возможных целочисленных значений переменной этого типа. Синтаксис перечисления
Enum <имя> { <имя поля 1>, <имя поля 2>, ... <имя поля N> }; //здесь стоит;!
Например
#include
В этой программе объявлено перечисление с именем Gender. Переменная типа enum Gender может принимать теперь только два значения – это MALE И FEMALE.
По умолчанию, первое поле структуры принимает численное значение 0, следующее 1, следующее 2 и т.д. Можно задать нулевое значение явно:
#include
Будут выведены значения 0 1 0 1 2. То есть, значение SYMBOL равно значению EXPRESSION, а NUMBER равно OPERATOR. Если мы изменим программу и напишем
Enum Token { SYMBOL, //0 NUMBER, //1 EXPRESSION = 10, //10 OPERATOR, //11 UNDEFINED //12 };
То SYMBOL будет равно значению 0, NUMBER равно 1, EXPRESSION равно 10, OPERATOR равно 11, UNDEFINED равно 12.
Принято писать имена полей перечисления, как и константы, заглавными буквами. Так как поля перечисления целого типа, то они могут быть использованы в операторе switch.
Заметьте, что мы не можем присвоить переменной типа Token просто численное значение. Переменная является сущностью типа Token и принимает только значения полей перечисления. Тем не менее, переменной числу можно присвоить значение поля перечисления.
Обычно перечисления используются в качестве набора именованных констант. Часто поступают следующим образом - создают массив строк, ассоциированных с полями перечисления. Например
#include
Так как поля принимают численные значения, то они могут использоваться в качестве индекса массива строк. Команда exit(N) должна получать код ошибки, отличный от нуля, потому что 0 - это плановое завершение без ошибки. Именно поэтому первое поле перечисления равно единице.
Перечисления используются для большей типобезопасности и ограничения возможных значений переменной. Для того, чтобы не писать enum каждый раз, можно объявить новый тип. Делается это также, как и в случае структур.
Typedef enum enumName { FIELD1, FIELD2 } Name;
Как известно, перечисления - это тип который может содержать значения указанные программистом. Целочисленные именованные константы могут быть определены как члены перечисления. Например:
Enum { RED, GREEN, BLUE }; определяет три целочисленные константы и присваивает им значения. По умолчанию, значения присваиваются по порядку начиная с нуля, т.е. RED == 0 , GREEN == 1 и BLUE == 2 . Перечисление также может быть именованным: enum color { RED, GREEN, BLUE }; Каждое перечисление - это отдельный тип, и тип каждого члена перечисления - это само перечисление. Например RED имеет тип color . Объявление типа переменной как color , вместо обычного unsigned , может подсказать и программисту и компилятору о том как эта переменная должна быть использована. Например: void f(color c) { switch(c){ case RED: // do something break; case BLUE: // do something break; } }
В этом случае компилятор может выдать предупреждение о том, что обрабатываются только два значения color из трёх возможных.
Таким образом перечисления это:
Создание именованных констант с автоматическим увеличением значения константы
Предупреждения о возможных ошибках со стороны компилятора
На самом деле всё что выше - общие слова, которые нужны только для того чтобы те кто забрёл сюда по ошибке, хотя бы что-то из этой статьи вынесли. А мы сейчас поговорим о сложностях и хитростях с которыми приходится сталкиваться каждому кто более-менее юзает перечисления в нормальном девелопменте. Итак, с чем приходится сталкиваться:
1. Отображение значения перечисления в строку которая совпадает с именем члена перечисления, т.е. что-либо что для enum_map вернёт "RED" .
2. Итерация по членам перечисления и контроль выхода за границы. Т.е. сколько бы вы не добавляли новых элементов в перечисление, у вас всегда есть константа которая ровно на единицу больше последнего члена последовательности.
3. (Тем, кто не прошёл тест по шаблонам, можно не читать) Отображение run-time целочисленной переменной в compile-time переменную или тип (указатель на функцию с определённым значением параметра шаблона etc.).
Разберём вышеозначенные задачи в деталях.
Оправданий для того чтобы использовать такой код может быть только два:
Уберите от экранов детей!
присутствуют сцены аморального и порнографического характера
Превратим то изящное перечисление, которое у нас было:
Enum color { RED, GREEN, BLUE };
вот в этого монстра Франкенштейна:
color.h
#include "enum_helper_pre.h" enumeration_begin (color) declare_member (RED) delimiter declare_member (GREEN) delimiter declare_member (BLUE) enumeration_end ; #include "enum_helper_post.h"
Выглядит пугающе, но на самом деле мы просто добавили возможность переопределить каждый элемент конструкции enum:
enum_helper_pre.h
#ifndef delimiter #define delimiter , #endif #ifndef enumeration_begin #define enumeration_begin (arg) enum arg { #endif #ifndef enumeration_end #ifdef last_enumerator #define enumeration_end delimiter last_enumerator } #else #define enumeration_end } #endif #endif #ifndef declare_member #define declare_member (arg) arg #endif #ifndef member_value #define member_value (arg) = arg #endif Думаю понятно что в конце для всех этих макросов нужно сделать #undef:
enum_helper_post.h
#undef delimiter #undef enumeration_begin #undef enumeration_end #undef last_enumerator #undef declare_member #undef member_value
Теперь если вам нужно заполнить некий массив строковыми представлениями членов перечисления, то выглядеть это будет так:
main.c
#include
Output
RED GREEN BLUE
Вуаля! Вот оно - три строчки кода и вы для любого перечисления, оформленного по нашим правилам, получаете массив из строковых представлений членов перечисления, независимо от количества членов перечисления. Любой человек который будет дебажить код по трейсам, в которых вместо безликих чисел будут стоять названия элементов, скажет вам огромное спасибо.
Опытный глаз заметит, что пример выше получился немного хромой, потому что перечислениям в крупных проектах свойственно расти с течением времени и верхнюю границу цикла приходится всё время переопределять. Например, если наше перечисление определяло компоненты цвета, то мы может добави в него компонент определяющий яркость цвета:
Enum color { RED, GREEN, BLUE, BRIGHTNESS };
Теперь в примере придётся изменить условие выполнение цикла на while(c <= BRIGHTNESS) . Если такой цикл один на всю программу, то в этом нет ничего страшного, но если подобный цикл встречается в десятке мест, то, рано или поздно, вы начнёте забывать все места где надо поменять предельное значение цикла, что приведёт к трудноуловимым ошибкам.
Другой пример, когда подобные проверки станут нашим кошмаром - валидация данных полученных из некоторого источника. Например сервер третьей стороны может передавать вам через сокет значения какого либо перечисления, и на каждое возможное значение вы выполняете различные действия. Представьте себе если администрация сервера добавила новый элемент в конец перечисления или же в сокет пришёл какой-либо мусор. Тогда вы заметите это лишь когда ваш клиент, слушающий сокет, отправит данные дальше и один из модулей, получив эти данные, рухнет. Решением в этом случае была бы проверка следующего вида:
If(c < RED || c > BRIGHTNESS) throw std::runtime_error("Wrong color received");
И опять же если вы добавите новый элемент в перечисление, вам придётся внести изменения во все файлы где есть такие проверки. Есть решение проще - добавить фиктивный член перечисления, с фиксированным именем, всегда являющийся посленим членом перечисления. С нашим определением файла color.h сделать это - проще простого:
main.c
#include
Output
RED GREEN BLUE BRIGHTNESS
Теперь вы можете добавлять сколько угодно членов в перечисление, не боясь ничего потерять (о неизменённых конструкциях switch вас предупредит компилятор).
Я уже говорил что будут шаблоны?
Теперь представьте себе, что у вас есть семейство шаблонных функций вида template
Void f(color c)
{
switch(c)
{
case RED:
f
Вопрос только в том сколько элементов может быть в перечислении и, опять же, сколько файлов вам придётся поправить, если в перечислении что-то изменится. Поэтому проще создать карту указателей на все эти функции, и отображать входное значение в указатель на функцию:
Void f(color c)
{
if(c < RED || c >= COLOR_END)
return;
typedef void (*func_type)();
#define enumeration_begin(arg) func_type func_map={
#define declare_member(arg) f
Значительно проще, не правда ли?
Усложним задачу. Теперь нам нужно не только отображать член перечисления в строку, но и наоборот. И при этом RED == -2 и BLUE == 5 . Используя стандартные перечисления добиться результата можно лишь напрямую забив данные в карту отображений.
Решение, на самом деле, очень простое, как и всё этой статье:
color.h
#include "enum_helper_pre.h" enumeration_begin(color) declare_member(RED) member_value(-2) delimiter declare_member(GREEN) delimiter declare_member(BLUE) member_value(5) delimiter declare_member(BRIGHTNESS) enumeration_end; #include "enum_helper_post.h"
main.cpp
#include Output
RED
BLUE
-1
6
Собственно получилось простенько, но со вкусом. Таким образом придерживаясь немного громоздкого стиля описания перечислений, вы можете достичь невероятной гибкости при их использовании. Вот собственно и всё что я хотел здесь рассказать, если у кого-то возникнут вопросы - я с радостью на них отвечу. Буду рад если кому-то эта статья облегчит нелёгкую программистскую жизнь. У перечислений есть множество способов применения в разработке. Например, при создании игр они используются для программирования состояний персонажа или возможных направлений движения: Enum State {Idle, Fidget, Walk, Scan, Attack};
enum Direction {North, South, East, West};
К сожалению, в С++ нет возможности легко конвертировать значения перечислений в строковые и обратно. Поэтому разработчики вынуждены прибегать к разным ухищрениям, которые требуют определённой поддержки: жёстко закодированным преобразованиям или к использованию неприглядного ограничительного синтаксиса, наподобие Х-макросов. Кто-то дополнительно использует средства сборки для автоматического преобразования. Естественно, это только усложняет процесс разработки. Ведь перечисления имеют свой собственный синтаксис и хранятся в собственных входных файлах, что не облегчает работу средств сборки в Makefile или файлах проекта. Однако средствами С++ можно гораздо проще решить задачу преобразования перечислений в строковые. BETTER_ENUM(State, int, Idle, Fidget, Walk, Scan, Attack)
BETTER_ENUM(Direction, int, North, South, East, West)
State state = State::Fidget;
state._to_string(); // "Fidget"
std::cout << "state: " << state; // Пишет "state: Fidget"
state = State::_from_string("Scan"); // State::Scan (3)
// Применяется в switch, как и обычное перечисление.
switch (state) {
case State::Idle:
// ...
break;
// ...
}
Помимо преобразования в строковые и обратно, а также поточного ввода/вывода, мы можем ещё и перебирать сгенерированные перечисления: For (Direction direction: Direction._values())
character.try_moving_in_direction(direction);
BETTER_ENUM(Flags, char, Allocated = 1, InUse = 2, Visited = 4, Unreachable = 8)
Flags::_size(); // 4
Вы можете скачать с Github пример реализации макроса, упакованного в библиотеку под названием Better Enums (Улучшенные перечисления). Она распространяется под лицензией BSD, так что с ней можно делать что угодно. В данной реализации имеется один заголовочный файл, так что использовать её очень просто, достаточно добавить enum.h в папку проекта. Попробуйте, возможно, это поможет вам в решении ваших задач. BETTER_ENUM(Direction, int, North = 1, South = 2, East = 4, West = 8)
Struct Direction {
enum _Enum: int {North = 1, South = 2, East = 4, West = 8};
static const int _values = {1, 2, 4, 8};
static const char * const _names = {"North", "South", "East", "West"};
int _value;
// ...Функции, использующие вышеприведённое объявление...
};
Static const int _values = {__VA_ARGS__};
Static const int _values = {North = 1, South = 2, East = 4, West = 8};
Template Static const int _values =
{(_eat<_Enum>)North = 1,
(_eat<_Enum>)South = 2,
(_eat<_Enum>)East = 4,
(_eat<_Enum>)West = 8};
Static const char * const _names =
{"North = 1", "South = 2", "East = 4", "West = 8"};
Удобное преобразование перечислений (enum) в строковые в С++
Гораздо удобнее, когда во время отладки в консоль выводится сообщение типа “ State: Fidget ” вместо “ State: 1 ”. Также частенько бывает нужно сериализировать перечисления в JSON, YAML или иной формат, причём в виде строковых значений. Помимо того, что строковые воспринимать легче, чем числа, их применение в формате сериализации повышает устойчивость к изменениям численных значений констант перечислений. В идеале, "Fidget" должен ссылаться на Fidget , даже если объявлена новая константа, а Fidget имеет значение, отличное от 1.
Есть возможность избежать всех упомянутых трудностей и генерировать перечисления с полной рефлексией на чистом С++. Объявление выглядит так:
Способ применения:
Это делается с помощью нескольких ухищрений, связанных с препроцессором и шаблоном. О них мы немного поговорим в конце статьи.
Можно сгенерировать перечисления с разреженными диапазонами, а затем подсчитать:
Если вы работаете в С++ 11, то можете даже сгенерировать код на основе перечислений, потому что все преобразования и циклы могут выполняться в ходе компиляции с помощью функций constexpr . Можно, к примеру, написать такую функцию constexpr , которая будет вычислять максимальное значение перечисления и делать его доступным во время компиляции. Даже если значения констант выбираются произвольно и не объявляются в порядке возрастания.Как это работает
Для осуществления преобразований между строковыми и значениями перечислений необходимо сгенерировать соответствующий маппинг. Better Enums делает это с помощью создания двух массивов в ходе компиляции. Например, если у вас есть такое объявление:
то макрос переделает его в нечто подобное:
А затем перейдет к преобразованию: найдет индекс значения или строковой в _values или _names и вернет его соответствующее значение или строковую в другой массив. Массив значений
_values генерируется путём обращения к константам внутреннего перечисления _Enum . Эта часть макроса выглядит так:
Она трансформируется в:
Это почти
правильное объявление массива. Проблема заключается в дополнительных инициализаторах вроде «= 1». Для работы с ними Better Enums определяет вспомогательный тип, предназначенный для оператора присваивания, но игнорирует само присваиваемое значение:
Теперь можно включить инициализатор «= 1» в выражение присваивания, не имеющее значения:Массив строковых
Для создания этого массива Better Enums использует (#) - препроцессорный оператор перевода в строковое (stringization). Он конвертирует __VA_ARGS__ в нечто подобное:
Теперь мы почти
преобразовали имена констант в строковые. Осталось избавиться от инициализаторов. Однако Better Enums этого не делает. Просто при сравнении строковых в массиве _names он воспринимает символы пробелов и равенства как дополнительные границы строк. Так что при поиске “ North = 1 ” Better Enums найдёт только “ North ”.Можно ли обойтись без макроса?
Вряд ли. Дело в том, что в С++ оператор (#) - единственный способ преобразования токена исходного кода в строковое. Так что в любой библиотеке, автоматически преобразующей перечисления с рефлексией, приходится использовать как минимум один высокоуровневый макрос.Прочие соображения
Конечно, полностью рассматривать реализацию макроса было бы гораздо скучнее и сложнее, чем это сделано в данной статье. В основном, сложности возникают из-за поддержки работающих с массивами static функций constexpr , из-за особенностей разных компиляторов. Также определённые затруднения могут быть связаны с разложением как можно большей части макроса на шаблоны ради ускорения компиляции (шаблоны не нужно репарсить в ходе создания, а расширения-макросы - нужно).