Правильный способ связывания перечислений со строками

Скажем, у меня есть ряд строк, которые я часто использую в своей программе (для хранения состояния и т.д.). Операции с строками могут быть дорогими, поэтому при обращении к ним я хотел бы использовать перечисление. До сих пор я видел пару решений:

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"