Сайт о телевидении

Сайт о телевидении

» » Перечисления enum. Перечисляемый тип

Перечисления enum. Перечисляемый тип

Теги: Си перечисление, enum.

Перечисляемый тип

В си выделен отдельный тип перечисление (enum), задающий набор всех возможных целочисленных значений переменной этого типа. Синтаксис перечисления

Enum <имя> { <имя поля 1>, <имя поля 2>, ... <имя поля N> }; //здесь стоит;!

Например

#include #include enum Gender { MALE, FEMALE }; void main() { enum Gender a, b; a = MALE; b = FEMALE; printf("a = %d\n", a); printf("b = %d\n", b); getch(); }

В этой программе объявлено перечисление с именем Gender. Переменная типа enum Gender может принимать теперь только два значения – это MALE И FEMALE.

По умолчанию, первое поле структуры принимает численное значение 0, следующее 1, следующее 2 и т.д. Можно задать нулевое значение явно:

#include #include enum Token { SYMBOL, //0 NUMBER, //1 EXPRESSION = 0, //0 OPERATOR, //1 UNDEFINED //2 }; void main() { enum Token a, b, c, d, e; a = SYMBOL; b = NUMBER; c = EXPRESSION; d = OPERATOR; e = UNDEFINED; printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); printf("d = %d\n", d); printf("e = %d\n", e); getch(); }

Будут выведены значения 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 #include #include static char *ErrorNames = { "Index Out Of Bounds", "Stack Overflow", "Stack Underflow", "Out of Memory" }; enum Errors { INDEX_OUT_OF_BOUNDS = 1, STACK_OVERFLOW, STACK_UNDERFLOW, OUT_OF_MEMORY }; void main() { //ошибка случилась printf(ErrorNames); exit(INDEX_OUT_OF_BOUNDS); }

Так как поля принимают численные значения, то они могут использоваться в качестве индекса массива строк. Команда 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 из трёх возможных.

Таким образом перечисления это:

Создание именованных констант с автоматическим увеличением значения константы

Предупреждения о возможных ошибках со стороны компилятора

Основные проблемы при использовании enum

На самом деле всё что выше - общие слова, которые нужны только для того чтобы те кто забрёл сюда по ошибке, хотя бы что-то из этой статьи вынесли. А мы сейчас поговорим о сложностях и хитростях с которыми приходится сталкиваться каждому кто более-менее юзает перечисления в нормальном девелопменте. Итак, с чем приходится сталкиваться:

1. Отображение значения перечисления в строку которая совпадает с именем члена перечисления, т.е. что-либо что для enum_map вернёт "RED" .

2. Итерация по членам перечисления и контроль выхода за границы. Т.е. сколько бы вы не добавляли новых элементов в перечисление, у вас всегда есть константа которая ровно на единицу больше последнего члена последовательности.

3. (Тем, кто не прошёл тест по шаблонам, можно не читать) Отображение run-time целочисленной переменной в compile-time переменную или тип (указатель на функцию с определённым значением параметра шаблона etc.).

Разберём вышеозначенные задачи в деталях.

Отображение членов перечисления в строки

Плохое решение: const char* const strs={"RED","GREEN","BLUE"}; void f(color c) { puts(strs[c]); }

Оправданий для того чтобы использовать такой код может быть только два:

  • Если стоит статическая проверка на равенство количества членов перечисления и количества элементов в массиве строк (о статических проверках можете прочитать у Александреску или дождаться моей следующей статьи, которая как раз будет написана по этой теме). Оправдание довольно слабое.
  • Если строки могут не совпадать с названиями членов перечисления (редкий, на самом деле, случай). Т.е. что-то типа такого: const char* const strs={"Red as an egypt rose","Green peace","Blue Screen of Death"}; .
Решение:

Уберите от экранов детей!
присутствуют сцены аморального и порнографического характера

Превратим то изящное перечисление, которое у нас было:

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 #define enumeration_begin(arg) const char* const arg##_strs={ #define declare_member(arg) #arg #include "color.h" #include "color.h" int main(int argc,char* argv) { unsigned c = RED; while(c <= BLUE) { puts(color_strs); } return 0; }

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 #define enumeration_begin(arg) const char* const arg##_strs={ #define declare_member(arg) #arg #include "color.h" #define last_enumerator COLOR_END #include "color.h" int main(int argc,char* argv) { unsigned c = RED; while(c < COLOR_END) { puts(color_strs); } return 0; }

Output

RED GREEN BLUE BRIGHTNESS

Теперь вы можете добавлять сколько угодно членов в перечисление, не боясь ничего потерять (о неизменённых конструкциях switch вас предупредит компилятор).

Отображение run-time в compile-time

Я уже говорил что будут шаблоны?

Теперь представьте себе, что у вас есть семейство шаблонных функций вида template void f() , и, допустим, опять же через сокет к вам приходит число, и это для этого числа (интерпретированного как color) нужно вызвать соответствущую функцию f . Простое решение напрашивается само собой:

Void f(color c) { switch(c) { case RED: f(); break; case GREEN: f(); break; case BLUE: f(); break; case BRIGHTNESS: f(); break; } }

Вопрос только в том сколько элементов может быть в перечислении и, опять же, сколько файлов вам придётся поправить, если в перечислении что-то изменится. Поэтому проще создать карту указателей на все эти функции, и отображать входное значение в указатель на функцию:

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 #include "color.h" func_map[c](); }

Значительно проще, не правда ли?

Обещанный пример

Усложним задачу. Теперь нам нужно не только отображать член перечисления в строку, но и наоборот. И при этом 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 #include #include #include #include "color.h" int main(int argc,char* argv) { typedef boost::bimap map_type; map_type color_map; #define declare_member(arg) color_map.insert(map_type::value_type(arg,BOOST_PP_STRINGIZE(arg))) #define delimiter ; #define enumeration_begin(arg) #define enumeration_end #define member_value(arg) #include "color.h" std::cout<

Output

RED BLUE -1 6

Собственно получилось простенько, но со вкусом. Таким образом придерживаясь немного громоздкого стиля описания перечислений, вы можете достичь невероятной гибкости при их использовании.

Вот собственно и всё что я хотел здесь рассказать, если у кого-то возникнут вопросы - я с радостью на них отвечу. Буду рад если кому-то эта статья облегчит нелёгкую программистскую жизнь.

24 февраля 2016 в 11:04

Удобное преобразование перечислений (enum) в строковые в С++

  • Блог компании NIX Solutions ,
  • C++ ,
  • Программирование
  • Перевод

У перечислений есть множество способов применения в разработке. Например, при создании игр они используются для программирования состояний персонажа или возможных направлений движения:

Enum State {Idle, Fidget, Walk, Scan, Attack}; enum Direction {North, South, East, West};
Гораздо удобнее, когда во время отладки в консоль выводится сообщение типа “ State: Fidget ” вместо “ State: 1 ”. Также частенько бывает нужно сериализировать перечисления в JSON, YAML или иной формат, причём в виде строковых значений. Помимо того, что строковые воспринимать легче, чем числа, их применение в формате сериализации повышает устойчивость к изменениям численных значений констант перечислений. В идеале, "Fidget" должен ссылаться на Fidget , даже если объявлена новая константа, а Fidget имеет значение, отличное от 1.

К сожалению, в С++ нет возможности легко конвертировать значения перечислений в строковые и обратно. Поэтому разработчики вынуждены прибегать к разным ухищрениям, которые требуют определённой поддержки: жёстко закодированным преобразованиям или к использованию неприглядного ограничительного синтаксиса, наподобие Х-макросов. Кто-то дополнительно использует средства сборки для автоматического преобразования. Естественно, это только усложняет процесс разработки. Ведь перечисления имеют свой собственный синтаксис и хранятся в собственных входных файлах, что не облегчает работу средств сборки в 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
Если вы работаете в С++ 11, то можете даже сгенерировать код на основе перечислений, потому что все преобразования и циклы могут выполняться в ходе компиляции с помощью функций constexpr . Можно, к примеру, написать такую функцию constexpr , которая будет вычислять максимальное значение перечисления и делать его доступным во время компиляции. Даже если значения констант выбираются произвольно и не объявляются в порядке возрастания.

Вы можете скачать с Github пример реализации макроса, упакованного в библиотеку под названием Better Enums (Улучшенные перечисления). Она распространяется под лицензией BSD, так что с ней можно делать что угодно. В данной реализации имеется один заголовочный файл, так что использовать её очень просто, достаточно добавить enum.h в папку проекта. Попробуйте, возможно, это поможет вам в решении ваших задач.

Как это работает

Для осуществления преобразований между строковыми и значениями перечислений необходимо сгенерировать соответствующий маппинг. Better Enums делает это с помощью создания двух массивов в ходе компиляции. Например, если у вас есть такое объявление:

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; // ...Функции, использующие вышеприведённое объявление... };
А затем перейдет к преобразованию: найдет индекс значения или строковой в _values или _names и вернет его соответствующее значение или строковую в другой массив.

Массив значений

_values генерируется путём обращения к константам внутреннего перечисления _Enum . Эта часть макроса выглядит так:

Static const int _values = {__VA_ARGS__};
Она трансформируется в:

Static const int _values = {North = 1, South = 2, East = 4, West = 8};
Это почти правильное объявление массива. Проблема заключается в дополнительных инициализаторах вроде «= 1». Для работы с ними Better Enums определяет вспомогательный тип, предназначенный для оператора присваивания, но игнорирует само присваиваемое значение:

Template struct _eat { T _value; template _eat& operator =(Any value) { return *this; } // Игнорирует аргумент. explicit _eat(T value) : _value(value) { } // Преобразует из T. operator T() const { return _value; } // Преобразует в T. }
Теперь можно включить инициализатор «= 1» в выражение присваивания, не имеющее значения:

Static const int _values = {(_eat<_Enum>)North = 1, (_eat<_Enum>)South = 2, (_eat<_Enum>)East = 4, (_eat<_Enum>)West = 8};

Массив строковых

Для создания этого массива Better Enums использует (#) - препроцессорный оператор перевода в строковое (stringization). Он конвертирует __VA_ARGS__ в нечто подобное:

Static const char * const _names = {"North = 1", "South = 2", "East = 4", "West = 8"};
Теперь мы почти преобразовали имена констант в строковые. Осталось избавиться от инициализаторов. Однако Better Enums этого не делает. Просто при сравнении строковых в массиве _names он воспринимает символы пробелов и равенства как дополнительные границы строк. Так что при поиске “ North = 1 ” Better Enums найдёт только “ North ”.

Можно ли обойтись без макроса?

Вряд ли. Дело в том, что в С++ оператор (#) - единственный способ преобразования токена исходного кода в строковое. Так что в любой библиотеке, автоматически преобразующей перечисления с рефлексией, приходится использовать как минимум один высокоуровневый макрос.

Прочие соображения

Конечно, полностью рассматривать реализацию макроса было бы гораздо скучнее и сложнее, чем это сделано в данной статье. В основном, сложности возникают из-за поддержки работающих с массивами static функций constexpr , из-за особенностей разных компиляторов. Также определённые затруднения могут быть связаны с разложением как можно большей части макроса на шаблоны ради ускорения компиляции (шаблоны не нужно репарсить в ходе создания, а расширения-макросы - нужно).