Темы, которые касаются не только тестов, IT и Quizful, вы можете создавать в новом разделе Обсуждения.

Вдобавок, появилась возможность комментировать профиль пользователя на странице профиля.

Надеемся, эти нововведения Вам понравятся.
Знаете ли Вы, что

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

Топ контрибуторов
loading
loading
Статистика

Тестов: 127, вопросов: 5126. Пройдено: 54989 / 186427.

Перечисления в C++ (c++ enum types)

head tail Информация о статье
категория
C++
дата15.05.2009
авторanalizer
голосов8

Мнение автора может не совпадать с его точкой зрения
(с) спёрто из ЖЖ

Рекомендуемая литература:

Введение в перечисления

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

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] вернёт "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 <stdio.h>

#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[c++]);
	}
	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 <stdio.h>

#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[c++]);
	}
	return 0;
}


Output

RED
GREEN
BLUE
BRIGHTNESS

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

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

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

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

void f(color c)
{
	switch(c)
	{
	case RED:
		f<RED>();
		break;
	case GREEN:
		f<GREEN>();
		break;
	case BLUE:
		f<BLUE>();
		break;
	case BRIGHTNESS:
		f<BRIGHTNESS>();
		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<arg>
	#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 <iostream>
#include <string>
#include <boost/bimap.hpp>
#include <boost/preprocessor/stringize.hpp>

#include "color.h"

int main(int argc,char* argv[])
{
	typedef boost::bimap<color,std::string> 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<<color_map.left.at(RED)<<std::endl;
	std::cout<<color_map.left.at(BLUE)<<std::endl;
	std::cout<<color_map.right.at("GREEN")<<std::endl;
	std::cout<<color_map.right.at("BRIGHTNESS")<<std::endl;
	return 0;
}


Output

RED
BLUE
-1
6

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

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

----

Дмитрий analizer Потапов, март 2009

Если Вам понравилась статья, проголосуйте за нее

Голосов: 8  loading...
admin   c0nst   md6   clumsy   zagra   zipu4   LeoBear   Engineer9  
Комментариев: 20
 slavikkk04.03.2010 | 12:47:33
Вообще говоря, использование макросов - есть абсолютное зло. :)

В свое время меня порадовала такая реализация enum -> string: http://vitamin-caig.livejournal.com/35202.html
ответить
 analizer04.03.2010 | 16:17:03
>>Вообще говоря, использование макросов - есть абсолютное зло. :)

Угу, разумеется, да и вообще код написанный не вами - говно.
ответить
 slavikkk04.03.2010 | 20:55:25
Только если он с макросами.
ответить
 MByte20.11.2009 | 17:04:14
Перечислений у меня порядком...пока 11 штук. С неймспейсами понял, спасибо. Я до вашего ответа успел сделать так: (имена переменных немного упрощены)
#define last_enum PLT_END // последний элемент перечисления PLACE
enum_begin(PLACE)
..............
enum_end;
#undef last_enum

#define last_enum OPT_END // последний элемент для перечисления OPERATIONTYPE
enum_begin(OPERATIONTYPE)
..............
enum_end;
#undef last_enum
ответить
 analizer20.11.2009 | 17:18:30
Как вариант, но при включении большого числа перечислений в один файл у вас не будет возможности разделить их.
Например если в конкретной функции вам потребуется переводить в строку члены какого-то перечисления, то в текущей области видимости у вас будут созданы массивы для перевода в текстовую форму для всех перечислений (см. последнюю вставку кода в статье).
Оптимизатор это конечно выкинет, но тем не менее если будете дебажить результат препроцессинга, то мешать будет сильно.

З.Ы. Каким образом вы планируете включить файл перечислений, задав last_enumerator для конкретного перечисления? (просто недавно это мне потребовалось и смог сделать это я только благодаря отдельному файлу для перечисления)
ответить
 MByte20.11.2009 | 19:43:38
Только что закончил ковыряться со всеми #define-ами и иже с ними...
По-моему мнению несколько не доработан сам текст статьи. Все определения макросов верны, только вот расставлять их по местам в коде целая морока. Я имею в виду правильную расстановку этих дефайнов:
1. #define enumeration_begin(arg) enum arg { - для преобразования в собственно в целые цисла
2. #define enumeration_begin(arg) const char* const arg##_strs[]={ - для преобразования в массив строк.
Если следовать тексту статьи, то определение №2 достаточно поместить в файл main.c (или .cpp?) и все. Но происходит ошибка компиляции. Как бы я ни пытался их куда-нибудь запихнуть, в начало файла с перечислениями, в конец (что, конечно же глупо, но чем черт не шутит )) ), затем в файл enum_helper_pre.h, тоже в оба конца )), ничего не получалось - ошибка компиляции. Оно и не удивительно - вперва определяется один enumeration_begin, а затем второй с этим же именем. Продолжение следует...
ответить
 MByte20.11.2009 | 19:51:49
На текущий момент получилось сделать так:
1. файл "Enumer.h" - содержит собственно ваши определения макросов для перевода перечислений в обычный, целочисленный формат, где содержится #define enum_begin(arg) typedef enum arg { и так далее.
2. файл "EnumerStr.h" - содержит определения макросов для перевода перечислений в массив строк, где содержится определение #define enum_begin(arg) const char* const arg##_strs[] = { и остальные дефайны.
3. файл "TextEnums.h" - содержит определения всех необходимых перечислений в формате:
#define last_enum PLT_END // последний элемент перечисления PLACE
enum_begin(PLACE) ..............enum_end;
#undef last_enum
#define last_enum OPT_END // последний элемент для перечисления OPERATIONTYPE
enum_begin(OPERATIONTYPE) ..............enum_end;
#undef last_enum

Продолжение следует...
ответить
 MByte20.11.2009 | 19:58:31
Ну, и наконец, имеем файл .h , который содержит следующие строки кода:
#include "Enumer.h" //перевод перечислений в целочисленный формат
#include "TextEnums.h" // список перечислений
#include "EnumerEnd.h" // #undef всё

#include "EnumerStr.h" //перевод перечислений в строковый формат
#include "TextEnums.h" // тот же список перечислений
#include "EnumerStrEnd.h" // #undef всё
и это работает )))
Отвечаю на ваш вопрос в ЗЫ - я сам интересовался у вас, как можно это сделать в комментарии за
16.11.2009 | 22:09:02
Было бы очень не плохо, если бы это делалось на автомате, а не в помощью постоянных #define перед, и #undef после каждого перечисления...
ответить
 analizer21.11.2009 | 10:08:09
И там же в "З.Ы." я написал что мне это удалось только вынесением перечисления в отдельный файл :)
ответить
 analizer21.11.2009 | 10:17:49
Хм... у меня вот работает тот код который приведен в статье (по крайней мере работал на момент написания).
Не вижу смысла создавать отдельный файл в котором будет определён маппинг из перечисления в строку, как правило я просто создаю маленькую функцию которая делает включение файла с перечислением в которой переопределен enumeration_begin и declare_member
ответить
 MByte16.11.2009 | 22:09:02
У меня другой вопрос: как быть с определением последнего элемента для нескольких разноименных перечислений в вашем случае?
Например имеем:
[B]#define last_ enumerator A_END

enumeration_begin(A)
.......
enumeration_end;
enumeration_begin(B)
.......
enumeration_end;[/B]

Это определение вызывает ошибку компиляции:
[B]'A_END' : redefinition; different basic types[/B]

Какое предложите решение?
Я так понимаю, что здесь:
[B]#define enumeration_end delimiter last_enumerator }[/B]
нужно сделать так, чтобы к определению last_enumerator автоматически добавлялось имя пречисления, т.е. так:
A_END, B_END и т.д.
Спасибо.
ответить
 analizer16.11.2009 | 23:14:06
Вообще предполагается что на каждое такое перечисление сделан либо отдельный файл, либо поместить эти перечисления в разные namespace'ы, либо если у вас таких перечислений всего два, одно поместить в безымянный namespace, а другое - нет.
ответить
 Sergeyy21.10.2009 | 19:44:26
По аналогии с COLOR_END можно ввести еще и COLOR_BEGIN, т.к. enum может расшириться не только в конце.
ответить
 analizer21.10.2009 | 19:51:52
Я тоже над этим когда-то думал, но перебрав в уме 4х летнюю практику, не нашёл ни единого примера когда у перечисления менялся первый элемент
ответить
 skochkarev23.06.2009 | 16:59:55
Никогда не понимал пляски с препроцессором. ИМХО противоречит самой сути C++. Да и сам себе способ хранения целочисленных значений в enum хромоват по сути. Для этого в C++ есть константы и mapы.
ответить
 analizer23.06.2009 | 17:34:21
Никогда не понимал людей которые пытаются критиковать, не предлагая лучшего. Имхо противоречит здравому смыслу.
Не понимаете плясок с препроцессором - прочитайте главу 16 стандарта 2003го года, там всё подробно описано.
Знаете как лучше - поведайте об этом способе миру, а лучше - напишите статью и опубликуйте. А я посмотрю как вы будете поддерживать уникальность хотя бы пары десятка констант в проекте. А ещё посмотрю как вы сумеете отлавливать ошибки в уникальности констант и соответствующих им значений на этапе компиляции.
ответить
 dsukhonin22.08.2009 | 23:42:29
Предлагаю использовать lisp. Противоречия со здравым смыслом имеются?
ответить
 analizer22.08.2009 | 23:58:20
Ага. Оффтоповый пост и толстый троллинг, в то время как комментарии к статьям были сделаны для выявления неточностей в статье.
ответить
 kislotnik12.11.2009 | 12:03:23
Видимо, задело за живое, если столько агрессии =)
ответить
 analizer12.11.2009 | 13:04:23
У меня всегда столько агрессии.
Выживших среди задевших за живое вообще не осталось.
ответить
Добавить комментарий