Использование 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;

Последнее обновление: 03.10.2018

Кроме примитивных типов данных в C# есть такой тип как enum или перечисление. Перечисления представляют набор логически связанных констант. Объявление перечисления происходит с помощью оператора enum . Далее идет название перечисления, после которого указывается тип перечисления - он обязательно должен представлять целочисленный тип (byte, int, short, long). Если тип явным образом не указан, то по умолчанию используется тип int. Затем идет список элементов перечисления через запятую:

Enum Days { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } enum Time: byte { Morning, Afternoon, Evening, Night }

В этих примерах каждому элементу перечисления присваивается целочисленное значение, причем первый элемент будет иметь значение 0, второй - 1 и так далее. Мы можем также явным образом указать значения элементов, либо указав значение первого элемента:

Enum Operation { Add = 1, // каждый следующий элемент по умолчанию увеличивается на единицу Subtract, // этот элемент равен 2 Multiply, // равен 3 Divide // равен 4 }

Но можно и для всех элементов явным образом указать значения:

Enum Operation { Add = 2, Subtract = 4, Multiply = 8, Divide = 16 }

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

Enum Color { White = 1, Black = 2, Green = 2, Blue = White // Blue = 1 }

Каждое перечисление фактически определяет новый тип данных. Затем в программе мы можем определить переменную этого типа и использовать ее:

Enum Operation { Add = 1, Subtract, Multiply, Divide } class Program { static void Main(string args) { Operation op; op = Operation.Add; Console.WriteLine(op); // Add Console.ReadLine(); } }

В программе мы можем присвоить значение этой переменной. При этом в качестве ее значения должна выступать одна из констант, определенных в данном перечислении. То есть несмотря на то, что каждая константа сопоставляется с определенным числом, мы не можем присвоить ей числовое значение, например, Operation op = 1; . И также если мы будем выводить на консоль значение этой переменной, то мы получим им константы, а не числовое значение. Если же необходимо получить числовое значение, то следует выполнить приведение к числовому типу:

Operation op; op = Operation.Multiply; Console.WriteLine((int)op); // 3

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

Enum Operation { Add = 1, Subtract, Multiply, Divide } class Program { static void Main(string args) { Operation op; op = Operation.Add; Console.WriteLine(op); // Add Console.ReadLine(); } }

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

Class Program { enum Operation { Add = 1, Subtract, Multiply, Divide } static void MathOp(double x, double y, Operation op) { double result = 0.0; switch (op) { case Operation.Add: result = x + y; break; case Operation.Subtract: result = x - y; break; case Operation.Multiply: result = x * y; break; case Operation.Divide: result = x / y; break; } Console.WriteLine("Результат операции равен {0}", result); } static void Main(string args) { // Тип операции задаем с помощью константы Operation.Add, которая равна 1 MathOp(10, 5, Operation.Add); // Тип операции задаем с помощью константы Operation.Multiply, которая равна 3 MathOp(11, 5, Operation.Multiply); Console.ReadLine(); } }

Здесь у нас имеется перечисление Operation, которое представляет арифметические операции. Также у нас определен метод MathOp, который в качестве параметров принимает два числа и тип операции. В основном методе Main мы два раза вызываем процедуру MathOp, передав в нее два числа и тип операции.

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

Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: