Что такое хороший справочник, документирующий шаблоны использования X-Macros в C (или, возможно, C ++)? - PullRequest
19 голосов
/ 05 ноября 2008

Базовое определение и пример, а также несколько ссылок для " X-Macros " приведены в этой записи в Википедии о препроцессоре C :

X-Macro - это заголовочный файл (обычно используя расширение ".def" вместо традиционный ".h"), который содержит список похожих макросов (которые могут именуемые «компонентными макросами»).

Каковы хорошие источники информации о том, как использовать эту мощную технику? Существуют ли хорошо известные библиотеки с открытым исходным кодом, использующие этот метод?

Ответы [ 3 ]

21 голосов
/ 05 ноября 2008

Я часто использую X Macros () в коде. Значение происходит только от добавления новых данных только в «список X» и без изменения какого-либо другого кода.

Чаще всего X Macros () используется для связи текста ошибки с кодами ошибок. Когда добавляются новые коды ошибок, программисты должны помнить, чтобы добавить код и текст, как правило, в отдельных местах. Макрос X позволяет добавлять новые данные об ошибках в одном месте и автоматически заполнять их в любом месте.

К сожалению, в механизмах используется много пре-компиляторской магии, которая может затруднить чтение кода (например, объединение строк с token1##token2, создание строк с #token). Поэтому я обычно объясняю, что делает X Macro в комментариях.

Вот пример использования ошибок / возвращаемых значений. Все новые данные добавляются в список "X_ERROR". Ни один другой код не должен быть изменен.

/* 
 * X Macro() data list
 * Format: Enum, Value, Text
 */
#define X_ERROR \
  X(ERROR_NONE,   1, "Success") \
  X(ERROR_SYNTAX, 5, "Invalid syntax") \
  X(ERROR_RANGE,  8, "Out of range")

/* 
 * Build an array of error return values
 *   e.g. {0,5,8}
 */
static int ErrorVal[] =
{
  #define X(Enum,Val,Text)     Val,
   X_ERROR
  #undef X
};

/* 
 * Build an array of error enum names
 *   e.g. {"ERROR_NONE","ERROR_SYNTAX","ERROR_RANGE"}
 */

static char * ErrorEnum[] = {
  #define X(Enum,Val,Text)     #Enum,
   X_ERROR
  #undef X
};

/* 
 * Build an array of error strings
 *   e.g. {"Success","Invalid syntax","Out of range"}
 */
static char * ErrorText[] = {
  #define X(Enum,Val,Text)     Text,
   X_ERROR
  #undef X
};

/* 
 * Create an enumerated list of error indexes
 *   e.g. 0,1,2
 */
enum {
  #define X(Enum,Val,Text)     IDX_##Enum,
   X_ERROR
  #undef X
  IDX_MAX   /* Array size */
};

void showErrorInfo(void)
{
    int i;

    /* 
     * Access the values
     */
    for (i=0; i<IDX_MAX; i++)
        printf(" %s == %d [%s]\n", ErrorEnum[i], ErrorVal[i], ErrorText[i]);

}

Вы также можете использовать X Macros () для генерации кода. Например, чтобы проверить, является ли значение ошибки «известным», макрос X может генерировать наблюдения в операторе switch:

 /*
  * Test validity of an error value
  *      case ERROR_SUCCESS:
  *      case ERROR_SYNTAX:
  *      case ERROR_RANGE:
  */

  switch(value)
  {

  #define X(Enum,Val,Text)     case Val:
   X_ERROR
  #undef X
         printf("Error %d is ok\n",value);
         break;
      default:
         printf("Invalid error: %d\n",value);
         break;
  }
10 голосов
/ 22 февраля 2012

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

/* declare an enumeration of state codes */
enum{ STATE0, STATE1, STATE2, ... , STATEX, NUM_STATES};

/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {func0, func1, func2, ... , funcX};

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

Мой друг познакомил меня с X-макросами, и у меня в голове погасла лампочка. Серьезно, где ты был всю свою жизнь x-macros!

Итак, теперь я определяю следующую таблицу:

#define STATE_TABLE \
        ENTRY(STATE0, func0) \
        ENTRY(STATE1, func1) \
        ENTRY(STATE2, func2) \
        ...
        ENTRY(STATEX, funcX) \

И я могу использовать его следующим образом:

enum
{
#define ENTRY(a,b) a,
    STATE_TABLE
#undef ENTRY
    NUM_STATES
};

и

p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
    STATE_TABLE
#undef ENTRY
};

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

#define ENTRY(a,b) static void b(void);
    STATE_TABLE
#undef ENTRY

Другое использование заключается в объявлении и инициализации регистров

#define IO_ADDRESS_OFFSET (0x8000)
#define REGISTER_TABLE\
    ENTRY(reg0, IO_ADDRESS_OFFSET + 0, 0x11)\
    ENTRY(reg1, IO_ADDRESS_OFFSET + 1, 0x55)\
    ENTRY(reg2, IO_ADDRESS_OFFSET + 2, 0x1b)\
    ...
    ENTRY(regX, IO_ADDRESS_OFFSET + X, 0x33)\

/* declare the registers (where _at_ is a compiler specific directive) */
#define ENTRY(a, b, c) volatile uint8_t a _at_ b:
    REGISTER_TABLE
#undef ENTRY

/* initialize registers */
#def ENTRY(a, b, c) a = c;
    REGISTER_TABLE
#undef ENTRY

Однако мое любимое использование - это когда дело доходит до обработчиков связи

Сначала я создаю таблицу связи, содержащую имя и код каждой команды:

#define COMMAND_TABLE \
    ENTRY(RESERVED,    reserved,    0x00) \
    ENTRY(COMMAND1,    command1,    0x01) \
    ENTRY(COMMAND2,    command2,    0x02) \
    ...
    ENTRY(COMMANDX,    commandX,    0x0X) \

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

Затем я также определяю структуры для каждой команды, чтобы определить, как выглядит каждая команда:

typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;

etc.

Аналогично, я определяю структуры для каждого ответа команды:

typedef struct {...}response1_resp_t;
typedef struct {...}response2_resp_t;

etc.

Тогда я могу определить перечисление кода моей команды:

enum
{
#define ENTRY(a,b,c) a##_CMD = c,
    COMMAND_TABLE
#undef ENTRY
};

Я могу определить мое перечисление длины команды:

enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
    COMMAND_TABLE
#undef ENTRY
};

Я могу определить мое перечисление длины ответа:

enum
{
#define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t);
    COMMAND_TABLE
#undef ENTRY
};

Я могу определить количество команд следующим образом:

typedef struct
{
#define ENTRY(a,b,c) uint8_t b;
    COMMAND_TABLE
#undef ENTRY
} offset_struct_t;

#define NUMBER_OF_COMMANDS sizeof(offset_struct_t)

ПРИМЕЧАНИЕ: я никогда не создаю экземпляр offset_struct_t, я просто использую его как способ, которым компилятор может сгенерировать для меня количество команд.

Обратите внимание, тогда я могу сгенерировать свою таблицу указателей функций следующим образом:

p_func_t jump_table[NUMBER_OF_COMMANDS] = 
{
#define ENTRY(a,b,c) process_##b,
    COMMAND_TABLE
#undef ENTRY
}

И мои прототипы функций:

#define ENTRY(a,b,c) void process_##b(void);
    COMMAND_TABLE
#undef ENTRY

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

/* reminder the sizeof a union is the size of its largest member */
typedef union
{
#define ENTRY(a,b,c) uint8_t b##_buf[sizeof(b##_cmd_t)];
    COMMAND_TABLE
#undef ENTRY
}tx_buf_t

Опять же, это объединение похоже на мою структуру смещения, оно не создается, вместо этого я могу использовать оператор sizeof для объявления размера моего буфера передачи.

uint8_t tx_buf[sizeof(tx_buf_t)];

Теперь мой буфер передачи tx_buf имеет оптимальный размер, и когда я добавляю команды в этот обработчик сообщений, мой буфер всегда будет иметь оптимальный размер. Круто!

5 голосов
/ 05 ноября 2008

Dr. У Dobb's есть статья на этом.

...