Мнение автора может не совпадать с его точкой зрения
(с) спёрто из ЖЖ
Рекомендуемая литература:
- C++ Programming Language, 3rd edition, Bjarne Stroustrup
- Tech Talk About C++ and C
- Рекомендуемый к прохождению тест - Тест C++ среднего уровня
Введение в перечисления
Как известно, перечисления - это тип который может содержать значения указанные программистом. Целочисленные именованные константы могут быть определены как члены перечисления. Например:
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
В свое время меня порадовала такая реализация enum -> string: http://vitamin-caig.livejournal.com/35202.html
Угу, разумеется, да и вообще код написанный не вами - говно.
#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
Например если в конкретной функции вам потребуется переводить в строку члены какого-то перечисления, то в текущей области видимости у вас будут созданы массивы для перевода в текстовую форму для всех перечислений (см. последнюю вставку кода в статье).
Оптимизатор это конечно выкинет, но тем не менее если будете дебажить результат препроцессинга, то мешать будет сильно.
З.Ы. Каким образом вы планируете включить файл перечислений, задав last_enumerator для конкретного перечисления? (просто недавно это мне потребовалось и смог сделать это я только благодаря отдельному файлу для перечисления)
По-моему мнению несколько не доработан сам текст статьи. Все определения макросов верны, только вот расставлять их по местам в коде целая морока. Я имею в виду правильную расстановку этих дефайнов:
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, а затем второй с этим же именем. Продолжение следует...
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
Продолжение следует...
#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 после каждого перечисления...
Не вижу смысла создавать отдельный файл в котором будет определён маппинг из перечисления в строку, как правило я просто создаю маленькую функцию которая делает включение файла с перечислением в которой переопределен enumeration_begin и declare_member
Например имеем:
[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 и т.д.
Спасибо.
Не понимаете плясок с препроцессором - прочитайте главу 16 стандарта 2003го года, там всё подробно описано.
Знаете как лучше - поведайте об этом способе миру, а лучше - напишите статью и опубликуйте. А я посмотрю как вы будете поддерживать уникальность хотя бы пары десятка констант в проекте. А ещё посмотрю как вы сумеете отлавливать ошибки в уникальности констант и соответствующих им значений на этапе компиляции.
Выживших среди задевших за живое вообще не осталось.