Основное определение, пример и несколько ссылок для "X-макросы" даны в этом запись в Википедии о препроцессоре C:
An X-Macro is a header file (commonly using a ".def" extension instead of the traditional ".h") that contains a list of similar macro calls (which can be referred to as "component macros").
Каковы хорошие источники информации о том, как использовать эту мощную технику? Существуют ли известные библиотеки с открытым исходным кодом, использующие этот метод?





У доктора Добба есть статья по этому поводу.
Я часто использую X Macros () в коде. Значение исходит только от добавления новых данных только в «список X» и без изменения какого-либо другого кода.
Чаще всего X Macros () используется для связывания текста ошибки с кодами ошибок. Когда добавляются новые коды ошибок, программисты должны не забывать добавлять код и текст, как правило, в разных местах. X Macro позволяет добавлять новые данные об ошибках в одном месте и автоматически заполнять их везде, где это необходимо.
К сожалению, в этих механизмах используется множество магических средств предварительной компиляции, которые могут затруднить чтение кода (например, соединение строк с помощью 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;
}
Очень, очень умно - это поможет не попасть в тиски зла, это улучшит как ремонтопригодность, так и удобочитаемость. Спасибо, JayG!
Забавно, я сам придумал похожий подход. Одна вещь, которую я обнаружил, заключается в том, что вам даже не нужно все это. Ссылаясь на ваш пример, вам даже не нужно #define или X_ERROR; вместо этого сделайте несколько строк "недействительных" X(). Затем я назвал файл, содержащий его, с помощью .x в конце (вместо, например, .h).
Я обнаружил 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-макросами, и это было похоже на то, как в моей голове загорелась лампочка. Серьезно, где вы всю жизнь были икс-макросы!
Итак, теперь я определяю следующую таблицу:
#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 имеет оптимальный размер, и когда я добавляю команды к этому обработчику связи, мой буфер всегда будет оптимального размера. Здорово!
У меня есть этот выпуск cuj, и он постоянно открыт для этой статьи.