Правильный способ связывания перечислений со строками
Скажем, у меня есть ряд строк, которые я часто использую в своей программе (для хранения состояния и т.д.). Операции с строками могут быть дорогими, поэтому при обращении к ним я хотел бы использовать перечисление. До сих пор я видел пару решений:
typedef enum {
STRING_HELLO = 0,
STRING_WORLD
} string_enum_type;
// Must be in sync with string_enum_type
const char *string_enumerations[] = {
"Hello",
"World"
}
Другой, с которым я встречаюсь довольно часто:
typedef enum {
STRING_HELLO,
STRING_WORLD
} string_enum_type;
const char *string_enumerations[] = {
[STRING_HELLO] = "Hello",
[STRING_WORLD] = "World"
}
Каковы минусы/плюсы этих двух методов? Есть ли лучший?
Ответы
Ответ 1
Единственное преимущество с первым заключается в том, что он обратно совместим с древними стандартами C.
Кроме того, последняя альтернатива является превосходной, поскольку она обеспечивает целостность данных, даже если изменение перечисления или изменение позиций. Однако он должен быть завершен проверкой, чтобы количество элементов в перечислении соответствовало количеству элементов в справочной таблице:
typedef enum {
STRING_HELLO,
STRING_WORLD,
STRING_N // counter
} string_enum_type;
const char *string_enumerations[] = {
[STRING_HELLO] = "Hello",
[STRING_WORLD] = "World"
};
_Static_assert(sizeof string_enumerations/sizeof *string_enumerations == STRING_N,
"string_enum_type does not match string_enumerations");
Вышеупомянутый метод является лучшим методом для простой связи "перечислительной таблицы". Другим вариантом будет использование структур, но это более подходит для более сложных типов данных.
И, наконец, в качестве дополнительной заметки, третьей версией будет использование "макросов X". Это не рекомендуется, если у вас нет специальных требований относительно повторения кода и обслуживания. Я включу его здесь для полноты, но я не рекомендую его в общем случае:
#define STRING_LIST \
/* index str */ \
X(STRING_HELLO, "Hello") \
X(STRING_WORLD, "World")
typedef enum {
#define X(index, str) index,
STRING_LIST
#undef X
STRING_N // counter
} string_enum_type;
const char *string_enumerations[] = {
#define X(index, str) [index] = str,
STRING_LIST
#undef X
};
_Static_assert(sizeof string_enumerations/sizeof *string_enumerations == STRING_N,
"string_enum_type does not match string_enumerations");
Ответ 2
Другой возможностью может быть использование функции вместо массива:
const char *enumtostring(string_enum_type e) {
switch(e) {
case STRING_HELLO: return "hello";
case STRING_WORLD: return "world";
}
}
gcc, по крайней мере, будет предупреждать, если вы добавите значение перечисления, но забудьте добавить соответствующий случай переключателя.
(Полагаю, вы могли бы попробовать сделать такую функцию inline
.)
Приложение: Предупреждение gcc, о котором я говорил, применяется только в том случае, если оператор switch
не имеет случая по default
. Поэтому, если вы хотите напечатать что-то для значений вне пределов, которые каким-то образом проползают, вы можете сделать это, а не по default
, но с чем-то вроде этого:
const char *enumtostring(string_enum_type e) {
switch(e) {
case STRING_HELLO: return "hello";
case STRING_WORLD: return "world";
}
return "(unrecognized string_enum_type value)";
}
Также неплохо включить значение вне пределов:
static char tmpbuf[50];
snprintf(tmpbuf, sizeof(tmpbuf), "(unrecognized string_enum_type value %d)", e);
return tmpbuf;
(У этого последнего фрагмента есть несколько дополнительных ограничений, но это дополнение уже давно наступило, поэтому я не буду с этим соглашаться.)
Ответ 3
Другая возможность - пользователь #defines
.
Несмотря на многие недостатки его использования, основное преимущество заключается в том, что #defines
занимают места, если они не используются...
#define STRING_HELLO "Hello"
#define STRING_WORLD "World"