Мнение автора может не совпадать с его точкой зрения
(с) спёрто из ЖЖ
Рекомендуемая литература:
- 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