Как оптимально перевести перечисления в наименьшем количестве кода?

Часто бывает так, что у вас есть 2 перечисления, которые эквивалентны, но пронумерованы по-разному, и вам нужны функции для перевода элементов из первого перечисления в элементы второго и наоборот. Обычно это очень утомительно для кода, и каждый раз, когда добавляется новый элемент, вы должны убедиться, что добавили пару как в функцию преобразования, так и в функцию обратного преобразования (это нарушает принцип DRY).

Какой наименее подверженный ошибкам способ сделать это, который по-прежнему генерирует эффективный код? Я упомянул эффективную часть, потому что вы могли бы просто создать кучу пар и поместить их в std::maps, если поиск во время выполнения не был проблемой. Я бы предпочел что-то столь же производительное, как ручное написание больших операторов переключения, переходящих от одного значения перечисления к другому.

Я уверен, что с некоторой магией препроцессора повышения или некоторым хакерством шаблонов вы могли бы придумать что-то, где вы пишете список пар и генерируются функции преобразования и обратного преобразования, но я не уверен, какой подход предпочесть и почему. Оба метода славятся медленным временем компиляции и сложностью диагностики ошибок компиляции. Или, может быть, какой-то другой подход?


person Joseph Garvin    schedule 07.07.2010    source источник
comment
Так часто, что я не думаю, что мне когда-либо приходилось это делать за более чем 25 лет программирования на C и C++.   -  person    schedule 07.07.2010
comment
Это странно... вам никогда не приходилось переводить между двумя библиотеками C, которые имеют некоторые перекрывающиеся наборы функций? Или перечисление «интерфейс» и перечисление «реализация» с дополнительными членами, между которыми нужно было переводить? Или одно перечисление, которое отображает, как что-то выходит из сокета, и другое перечисление, как ваше приложение ожидает, что все будет?   -  person Joseph Garvin    schedule 07.07.2010
comment
Можете ли вы привести пример того, как они нумеруются по-разному. Является ли основная часть набора в том же порядке и просто смещена, или они переставлены и т. д.   -  person Greg Domjan    schedule 07.07.2010
comment
Они находятся со смещением и переставлены. Просто добавить смещение не получится.   -  person Joseph Garvin    schedule 07.07.2010
comment
@Neil: см. мой комментарий к ответу Дэна Олсона, чтобы увидеть пример того, как это может произойти.   -  person Joseph Garvin    schedule 07.07.2010


Ответы (6)


Вы ищете что-то подобное? Не проверял, но должно работать.

(Применяются стандартные предупреждения о преждевременной оптимизации и необходимости профилирования; поиск std::map может быть не таким уж плохим, гигантская таблица переключения может быть не такой уж хорошей.)

перечисления-импл.ч:

// No include guard.
DEFINE_ENUM_PAIR(EGA_BRIGHT_RED, 12, HTML_RED, 0xff0000)
DEFINE_ENUM_PAIR(EGA_BRIGHT_BLUE, 9, HTML_BLUE, 0x0000ff)
DEFINE_ENUM_PAIR(EGA_BRIGHT_GREEN, 10, HTML_GREEN, 0x00ff00)
DEFINE_ENUM_PAIR(EGA_BLACK, 0, HTML_BLACK, 0x000000)

enums.cpp:

enum EgaColorType {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) name1 = value1,
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
};

enum HtmlColorType {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) name2 = value2,
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR 
};

HtmlColorType ConvertEgaToHtml(EgaColorType c) {
switch (c) {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) case name1: return name2;
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
default: assert(false);
}

EgaColorType ConvertHtmlToEga(HtmlColorType c) {
switch (c) {
#define DEFINE_ENUM_PAIR(name1, value1, name2, value2) case name2: return name1;
#include "enums-impl.h"
#undef DEFINE_ENUM_PAIR
default: assert(false);
}
person Josh Kelley    schedule 07.07.2010
comment
Это в основном магия препроцессора повышения, за исключением повторного изобретения препроцессора повышения. Взгляните, это гораздо менее уродливо, чем делать макросы X самостоятельно: boost.org/doc/libs/1_43_0/libs/preprocessor/doc/index.html - person Joseph Garvin; 07.07.2010
comment
Вам нужно решение, которое позволяет избежать медленного времени компиляции и трудно диагностируемых ошибок Boost.Preprocessor. Это решение делает это. Boost.Preprocessor — хороший инструмент, и я использовал его раньше, я просто не уверен, что его сложность и накладные расходы оправданы в данном случае. - person Josh Kelley; 07.07.2010
comment
Err, AFAICT, это та же техника, которую Boost.Preprocessor использует внутри; Я не понимаю, как это будет быстрее компилироваться. - person Joseph Garvin; 07.07.2010
comment
Boost.Preprocessor использует те же методы внутри, но чтобы представить его приятный интерфейс, он полагается на большое количество файлов заголовков, сотни определений макросов, расширения расширений расширений и т. д. Я видел макросы Boost.Preprocessor, которые занимают десятки тысячи шагов для полного расширения. Я сомневаюсь, что то, что вы пытаетесь сделать (как указано в ответе @Matthieu M.), будет настолько плохим, но я сомневаюсь, что это будет так же быстро, как простое включение файла несколько раз. - person Josh Kelley; 07.07.2010

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

person Edward Strange    schedule 07.07.2010
comment
Таблица поиска будет работать нормально, если вы сможете сгенерировать двунаправленную константную таблицу. В противном случае вы платите за время выполнения за информацию, известную во время компиляции. - person Joseph Garvin; 07.07.2010
comment
Поэтому инициализируйте таблицу поиска при запуске программы; единовременная стоимость запуска только во время выполнения должна быть незначительной. Или создайте константную таблицу поиска плюс определения перечисления из сценария, если вы хотите избежать всех затрат времени выполнения. - person Josh Kelley; 07.07.2010
comment
Вы имеете в виду двунаправленный, поскольку их два? - person Edward Strange; 07.07.2010

Как сказал Нил, я никогда не сталкивался с этой проблемой сам. Однако я столкнулся с проблемой кодовых наборов (т.е. сопоставление строки с перечислением и обратно).

Моя первая реакция эпидермальная: СУХАЯ.

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

Моей второй реакцией было бы попытаться удалить перечисление. Я не люблю перечисления. Возможно, я оценю их в грядущем C++0x, но в нынешнем виде они доставляют больше хлопот, чем того стоят, на мой взгляд. Я предпочитаю смарт-объекты с категориями и т. д.

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

Шаблоны здесь мало что могут сделать. Я использовал их для проверки диапазона в перечислениях и для преобразования строк (туда и обратно) и итераций, но это все, что они могут сделать. Однако, как вы подозревали, это возможно с помощью некоторого «тонкого» применения Boost.Preprocessor.

Что вы хотите написать:

DEFINE_CORRESPONDING_ENUMS(Server, Client,
  ((Server1, 1, Client1, 6))
  ((Server2, 2, Client2, 3))
  ((Common1, 4, Common1, 4))
  ((Common2, 5, Common2, 5))
  ((Server3, 7, Client3, 1))
);

И мы хотели бы, чтобы он сгенерировал:

struct Server
{
  enum type { Server1 = 1, Server2 = 2, Common1 = 4, Common2 = 5, Server3 = 7 };
};

struct Client
{
  enum type { Client1 = 6, Client2 = 3, Common1 = 4, Common2 = 5, Client3 = 1 };
};

Server::type ServerFromClient(Client::type c)
{
  switch(c)
  {
  case Client1: return Server1;
  //...
  default: abort();
  }
}

Client::type ClientFromServer(Server::type s)
{
  //...
}

Хорошая новость в том, что это возможно. Я мог бы даже это сделать, хотя, вероятно, я позволю вам немного поработать над этим;)

Вот некоторые пояснения:

  • Третий элемент макроса — это последовательность. Последовательность имеет неограниченный размер.
  • Каждый элемент последовательности является четверкой. Вам нужно заранее знать его размер, поэтому повторение для Common. Если бы вы вместо этого использовали последовательность, вы могли бы иметь дело с переменным количеством элементов (например, чтобы избежать повторения общего...), но это значительно усложнило бы ситуацию.
  • Вам нужно посмотреть на BOOST_PP_SEQ_FOREACH, здесь это будет основная операция.
  • Не забудьте BOOST_PP_CAT для обработки конкатенации токенов.
  • на gcc опция -E дает вывод препроцессора, это может пригодиться...
  • не забывайте комментарии и как их использовать в файле, иначе ваши коллеги будут вас ненавидеть
person Matthieu M.    schedule 07.07.2010
comment
Не связанные: имена, оканчивающиеся на _t, зарезервированы. - person Jon Purdy; 07.07.2010
comment
@Джон Парди: ах дерьмо! Я знал только о *__* и _[A-Z]* :/ Тогда я просто удалю определения типов. - person Matthieu M.; 08.07.2010

Если диапазоны перечисления относительно плотные (а не используются в качестве индикаторов растрового изображения), вы можете просто использовать массив для сопоставления. Вы позволяете компилятору определить длину массива, а затем можете утверждать, что длина не соответствует вашим требованиям. Возможно, вы даже сможете использовать static_assert, я не уверен. Поскольку вы используете массивы, преобразование должно быть постоянным по времени и, возможно, лучше, чем переключатель, если компилятор не генерирует внутреннюю таблицу переходов. Обратите внимание, что этот код полностью не проверен.

enum A
{
    MIN_A = 1,
    A_ATT_1 = 1,
    A_ATT_2 = 2,
    A_ATT_3 = 3,
    LAST_A
};

enum B
{
    MIN_B = 2
    B_ATT_2 = 2,
    B_ATT_1 = 4,
    B_ATT_3 = 5,
    LAST_B
};

B A_to_B[] =
{
    B_ATT_1,
    B_ATT_2,
    B_ATT_3
};

// Somewhere that will always run, as desired:
assert(LAST_A - MIN_A == sizeof(A_to_B) / sizeof(A_to_B[0]);

B from_A(A in)
{
    B ret = A_to_B[in - MIN_A];
    assert(ret != LAST_B);
    return ret;
}

A B_to_A[] =
{
    A_ATT_2,
    LAST_A,
    A_ATT_1,
    A_ATT_3
};

// Somewhere that will always run, as desired:
assert(LAST_B - MIN_B == sizeof(B_to_A) / sizeof(B_to_A[0]);

A from_B(B in)
{
    A ret = B_to_A[in - MIN_B];
    assert(ret != LAST_A);
    return ret;
}
person Mark B    schedule 07.07.2010

Ну, вы всегда можете попытаться сделать функцию (в смысле математической функции, а не функции программирования), которая переводит число одного перечисления в другое. Но эта функция будет меняться каждый раз, когда вы добавляете элементы.

person cake    schedule 07.07.2010

Не используйте два перечисления.

Между ними нет большой разницы:

enum FirstSet { A=4, B=6, C=8, D=5 };
enum SecondSet { E=2, F=5, G=5, H=1 };

и это:

enum OneEnum { A, B, C, D };
enum TwoEnum { E, F, G, H };
int FirstSet[] = { 4, 6, 8, 5 };
int SecondSet[] = { 2, 5, 5, 1 };

Количество доступов, которые необходимо изменить, может быть непомерно высоким, но это немного лучше, чем поиск O(n) каждый раз, когда вы хотите преобразовать.

person Dan Olson    schedule 07.07.2010
comment
К сожалению, это не в моих руках. Рассмотрим API, который работает на основе передачи пользователем значений из перечисления EnumA. Первоначально API реализован с помощью серверной части, использующей EnumA. Позже вы решаете использовать другую реализацию, в которой используется совершенно другое перечисление, EnumB. Для обеспечения обратной совместимости вам необходимо иметь возможность переводить EnumA‹->EnumB. - person Joseph Garvin; 07.07.2010