Как получить отражающую функциональность в C, без X-макросов

В связи с этим вопросом о программной инженерии о простой сериализации различного содержимого структуры по требованию, я обнаружил статью, в которой используются x-макросы для создания структурных метаданных, необходимых для сериализации структуры "из коробки". Я также видел похожие методы для "умных перечислений", но это сводится к тому же принципу, получая строковое представление перечисления, или значение поля структуры по его имени, или что-то подобное.

Однако опытные программисты на C в Stack Overflow утверждают, что x-макросы следует избегать как "последнее средство":

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

Возможно, правильный ответ - это что-то вроде протокола Buffers? Но зачем создавать определение структуры на другом языке (.proto определения), а затем выполнить шаг сборки для генерации файлов C предпочтительнее, чем использовать встроенный препроцессор для того же? И проблема в том, что эти методы все еще не позволяют мне извлечь одну структуру по имени, я должен разделить одно и то же определение между двумя проектами и поддерживать их синхронизацию.

Таким образом, вопрос заключается в следующем: если x-макросы являются "последним средством", то какой подход к моей проблеме (легко сериализовать различные внутренние данные при запросе с другого устройства) будет "первым средством" или что-нибудь еще до обращения к макрокоманде?

2 ответа

С помощью некоторой магии препроцессора, взятой из Boost, мы можем сделать макрос, способный генерировать отражаемые перечисления.

Мне удалось построить простую реализацию концепции, представленную ниже.


Во-первых, использование. Следующий:

ReflEnum(MyEnum,
    (first)
    (second , 42)
    (third)
)

Расширяется до:

enum MyEnum
{
    first,
    second = 42,
    third,
};

const char *EnumToString_MyEnum(enum MyEnum param)
{
    switch (param)
    {
      case first:
        return "first";
      case second:
        return "second";
      case third:
        return "third";
      default:
        return "<invalid>";
    }
}

Таким образом, полная программа может выглядеть так:

#include <stdio.h>

/*
 * Following is generated by the below ReflEnum():
 *   enum MyEnum {first, second = 42, third};
 *   const char *EnumToString_MyEnum(enum MyEnum value) {}
*/
ReflEnum(MyEnum,
    (first)
    (second , 42)
    (third)
)

int main()
{
    enum MyEnum foo = second;
    puts(EnumToString_MyEnum(foo));  // -> "second"
    puts(EnumToString_MyEnum(43));   // -> "third"
    puts(EnumToString_MyEnum(9001)); // -> "<invalid>"
}

А вот и сама реализация.

Он состоит из двух частей. Сам код и магический заголовок препроцессора бесстыдно сорваны с Boost.

Код:

#define ReflEnum_impl_Item(...) PPUTILS_VA_CALL(ReflEnum_impl_Item_, __VA_ARGS__)(__VA_ARGS__)
#define ReflEnum_impl_Item_1(name)        name,
#define ReflEnum_impl_Item_2(name, value) name = value,

#define ReflEnum_impl_Case(...) case PPUTILS_VA_FIRST(__VA_ARGS__): return PPUTILS_STR(PPUTILS_VA_FIRST(__VA_ARGS__));

#define ReflEnum(name, seq) \
    enum name {PPUTILS_SEQ_APPLY(seq, ReflEnum_impl_Item)}; \
    const char *EnumToString_##name(enum name param) \
    { \
        switch (param) \
        { \
            PPUTILS_SEQ_APPLY(seq, ReflEnum_impl_Case) \
            default: return "<invalid>"; \
        } \
    }

Не должно быть слишком сложно расширить код для поддержки преобразования string->enum; спросите в комментариях, если вы не уверены.

Магия:

Обратите внимание, что магия препроцессора должна генерироваться скриптом, и вы должны выбрать максимальный размер перечисления при его генерации. Поколение легко и оставлено в качестве упражнения для читателя.

Boost по умолчанию размер до 64, код ниже был сгенерирован для размера 4,

#define PPUTILS_E(...) __VA_ARGS__

#define PPUTILS_VA_FIRST(...) PPUTILS_VA_FIRST_IMPL_(__VA_ARGS__,)
#define PPUTILS_VA_FIRST_IMPL_(x, ...) x

#define PPUTILS_PARENS(...) (__VA_ARGS__)
#define PPUTILS_DEL_PARENS(...) PPUTILS_E __VA_ARGS__

#define PPUTILS_CC(a, b) PPUTILS_CC_IMPL_(a,b)
#define PPUTILS_CC_IMPL_(a, b) a##b

#define PPUTILS_CALL(macro, ...) macro(__VA_ARGS__)

#define PPUTILS_VA_SIZE(...) PPUTILS_VA_SIZE_IMPL_(__VA_ARGS__,4,3,2,1,0)
#define PPUTILS_VA_SIZE_IMPL_(i1,i2,i3,i4,size,...) size

#define PPUTILS_STR(...) PPUTILS_STR_IMPL_(__VA_ARGS__)
#define PPUTILS_STR_IMPL_(...) #__VA_ARGS__

#define PPUTILS_VA_CALL(name, ...) PPUTILS_CC(name, PPUTILS_VA_SIZE(__VA_ARGS__))

#define PPUTILS_SEQ_CALL(name, seq) PPUTILS_CC(name, PPUTILS_SEQ_SIZE(seq))

#define PPUTILS_SEQ_DEL_FIRST(seq) PPUTILS_SEQ_DEL_FIRST_IMPL_ seq
#define PPUTILS_SEQ_DEL_FIRST_IMPL_(...)

#define PPUTILS_SEQ_FIRST(seq) PPUTILS_DEL_PARENS(PPUTILS_VA_FIRST(PPUTILS_SEQ_FIRST_IMPL_ seq,))
#define PPUTILS_SEQ_FIRST_IMPL_(...) (__VA_ARGS__),

#define PPUTILS_SEQ_SIZE(seq) PPUTILS_CC(PPUTILS_SEQ_SIZE_0 seq, _VAL)
#define PPUTILS_SEQ_SIZE_0(...) PPUTILS_SEQ_SIZE_1
#define PPUTILS_SEQ_SIZE_1(...) PPUTILS_SEQ_SIZE_2
#define PPUTILS_SEQ_SIZE_2(...) PPUTILS_SEQ_SIZE_3
#define PPUTILS_SEQ_SIZE_3(...) PPUTILS_SEQ_SIZE_4
#define PPUTILS_SEQ_SIZE_4(...) PPUTILS_SEQ_SIZE_5
// Generate PPUTILS_SEQ_SIZE_i
#define PPUTILS_SEQ_SIZE_0_VAL 0
#define PPUTILS_SEQ_SIZE_1_VAL 1
#define PPUTILS_SEQ_SIZE_2_VAL 2
#define PPUTILS_SEQ_SIZE_3_VAL 3
#define PPUTILS_SEQ_SIZE_4_VAL 4
// Generate PPUTILS_SEQ_SIZE_i_VAL

#define PPUTILS_SEQ_APPLY(seq, macro) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, seq)(macro, seq)
#define PPUTILS_SEQ_APPLY_0(macro, seq)
#define PPUTILS_SEQ_APPLY_1(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq))
#define PPUTILS_SEQ_APPLY_2(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq)) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, PPUTILS_SEQ_DEL_FIRST(seq))(macro, PPUTILS_SEQ_DEL_FIRST(seq))
#define PPUTILS_SEQ_APPLY_3(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq)) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, PPUTILS_SEQ_DEL_FIRST(seq))(macro, PPUTILS_SEQ_DEL_FIRST(seq))
#define PPUTILS_SEQ_APPLY_4(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq)) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, PPUTILS_SEQ_DEL_FIRST(seq))(macro, PPUTILS_SEQ_DEL_FIRST(seq))
// Generate PPUTILS_SEQ_APPLY_i

"Первый курорт", как правило, будет одним из:

  • Сгруппируйте все свои данные в таблицы, сделанные из массивов / структур, предпочтительно только для чтения, как в первом связанном примере. Индекс таблицы используется в качестве ключа поиска для хранения данных ("первичный ключ" для использования терминов СУБД). Это быстро и понятно, но нужно соблюдать осторожность при обслуживании.

  • Сгруппируйте ваши данные в соответствии с некоторыми ОО-дизайнами. Вы можете использовать непрозрачные указатели и указатели функций для достижения приватной инкапсуляции и полиморфизма. При правильном использовании это может дать современный дизайн программы. Но в то же время это может быть несколько обременительно писать. А если вы не можете использовать динамическое выделение памяти (встроенные системы), то вам нужно изобрести пул памяти для каждого класса. Лучше всего подходит для более сложных контейнеров типа ADT и для проектирования API.

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

Из приведенного примера кода строка #define X(dir) {dir, #dir} возможно, следует прокомментировать более правильно, как это:

/*
  Create a temporary X-macro that expands the DIRECTION_LIST, to form 
  an array initialization list. The format will be:

  {north, "north"},  
  {south, "south"},
  ...
*/
#define X(dir) {dir, #dir}
  DIRECTION_LIST
#undef X
Другие вопросы по тегам