Я только что узнал о X-Macros. Какие виды использования X-Macros в реальном мире вы видели? Когда они являются подходящим инструментом для работы?
Использование X-Macros в реальном мире
Ответ 1
Я обнаружил 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-макросы!
Итак, теперь я определяю следующую таблицу:
#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 */
#define ENTRY(a, b, c) a = c;
REGISTER_TABLE
#undef ENTRY
Мое любимое использование, однако, когда дело доходит до обработчиков связи
Сначала я создаю таблицу comms, содержащую каждое имя и код команды:
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X) \
У меня есть как прописные, так и строчные имена в таблице, потому что верхний регистр будет использоваться для перечислений и строчных имен функций.
Затем я также определяю structs для каждой команды, чтобы определить, как выглядит каждая команда:
typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;
etc.
Аналогично, я определяю структуры для каждого ответа команды:
typedef struct {...}command1_resp_t;
typedef struct {...}command2_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 является оптимальным размером, и когда я добавляю команды этому обработчику comms, мой буфер всегда будет оптимальным размером. Круто!
Еще одно использование - создавать таблицы смещения: Поскольку память часто является ограничением для встроенных систем, я не хочу использовать 512 байт для моей таблицы перехода (2 байта на каждый указатель X 256 возможных команд), когда это разреженный массив. Вместо этого у меня будет таблица с 8-битными смещениями для каждой возможной команды. Это смещение затем используется для индексации моей фактической таблицы перехода, которая теперь должна быть только NUM_COMMANDS * sizeof (указатель). В моем случае с 10 заданными командами. Моя таблица перехода составляет 20 байтов, и у меня есть таблица смещения длиной 256 байтов, что составляет в общей сложности 276 байт вместо 512 байт. Затем я вызываю свои функции следующим образом:
jump_table[offset_table[command]]();
вместо
jump_table[command]();
Я могу создать таблицу смещения следующим образом:
/* initialize every offset to 0 */
static uint8_t offset_table[256] = {0};
/* for each valid command, initialize the corresponding offset */
#define ENTRY(a,b,c) offset_table[c] = offsetof(offset_struct_t, b);
COMMAND_TABLE
#undef ENTRY
где offsetof - стандартный библиотечный макрос, определенный в "stddef.h"
В качестве побочного преимущества существует очень простой способ определить, поддерживается ли командный код или нет:
bool command_is_valid(uint8_t command)
{
/* return false if not valid, or true (non 0) if valid */
return offset_table[command];
}
Вот почему в моем COMMAND_TABLE я зарезервировал байт команды 0. Я могу создать одну функцию под названием "process_reserved()", которая будет вызываться, если какой-либо недопустимый командный байт используется для индексации в моей таблице смещения.
Ответ 2
X-Макросы - это по существу параметризованные шаблоны. Поэтому они являются подходящим инструментом для работы, если вам нужно несколько подобных вещей в нескольких обличьях. Они позволяют создавать абстрактную форму и создавать ее в соответствии с различными правилами.
Я использую X-макросы для вывода значений перечисления в виде строк. И, встречаясь с этим, я сильно предпочитаю эту форму, которая берет "пользовательский" макрос для применения к каждому элементу. Множественное включение файлов гораздо более болезненно для работы.
/* x-macro constructors for error and type
enums and string tables */
#define AS_BARE(a) a ,
#define AS_STR(a) #a ,
#define ERRORS(_) \
_(noerror) \
_(dictfull) _(dictstackoverflow) _(dictstackunderflow) \
_(execstackoverflow) _(execstackunderflow) _(limitcheck) \
_(VMerror)
enum err { ERRORS(AS_BARE) };
char *errorname[] = { ERRORS(AS_STR) };
/* puts(errorname[(enum err)limitcheck]); */
Я также использую их для отправки функций на основе типа объекта. Опять же, захватив один и тот же макрос, я использовал для создания значений перечисления.
#define TYPES(_) \
_(invalid) \
_(null) \
_(mark) \
_(integer) \
_(real) \
_(array) \
_(dict) \
_(save) \
_(name) \
_(string) \
/*enddef TYPES */
#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };
Использование макроса гарантирует, что все мои индексы массива будут соответствовать связанным значениям перечисления, потому что они создают их различные формы, используя голые токены из определения макроса (макрос TYPES).
typedef void evalfunc(context *ctx);
void evalquit(context *ctx) { ++ctx->quit; }
void evalpop(context *ctx) { (void)pop(ctx->lo, adrent(ctx->lo, OS)); }
void evalpush(context *ctx) {
push(ctx->lo, adrent(ctx->lo, OS),
pop(ctx->lo, adrent(ctx->lo, ES)));
}
evalfunc *evalinvalid = evalquit;
evalfunc *evalmark = evalpop;
evalfunc *evalnull = evalpop;
evalfunc *evalinteger = evalpush;
evalfunc *evalreal = evalpush;
evalfunc *evalsave = evalpush;
evalfunc *evaldict = evalpush;
evalfunc *evalstring = evalpush;
evalfunc *evalname = evalpush;
evalfunc *evaltype[stringtype/*last type in enum*/+1];
#define AS_EVALINIT(_) evaltype[_ ## type] = eval ## _ ;
void initevaltype(void) {
TYPES(AS_EVALINIT)
}
void eval(context *ctx) {
unsigned ades = adrent(ctx->lo, ES);
object t = top(ctx->lo, ades, 0);
if ( isx(t) ) /* if executable */
evaltype[type(t)](ctx); /* <--- the payoff is this line here! */
else
evalpush(ctx);
}
Использование X-макросов таким образом помогает компилятору давать полезные сообщения об ошибках. Я опустил функцию evalarray из приведенного выше, потому что это будет отвлекать от моей точки. Но если вы попытаетесь скомпилировать вышеуказанный код (комментируя другие вызовы функций и, конечно же, предоставляя фиктивный typedef для контекста), компилятор будет жаловаться на недостающую функцию. Для каждого нового типа я добавляю, мне пришло в голову добавить обработчик, когда я перекомпилирую этот модуль. Таким образом, X-macro помогает гарантировать, что параллельные структуры остаются нетронутыми даже по мере роста проекта.
Edit:
Этот ответ поднял мою репутацию на 50%. Так вот немного больше. Ниже приведен отрицательный пример, отвечая на вопрос: когда не использовать X-макросы?
В этом примере показана упаковка произвольных фрагментов кода в X-запись. В конце концов я отказался от этой ветки проекта и не использовал эту стратегию в более поздних проектах (и не из-за попытки попробовать). Это как-то стало немыслимо. Действительно, макрос называется X6, потому что в какой-то момент было 6 аргументов, но я устал от изменения имени макроса.
/* Object types */
/* "'X'" macros for Object type definitions, declarations and initializers */
// a b c d
// enum, string, union member, printf d
#define OBJECT_TYPES \
X6( nulltype, "null", int dummy , ("<null>")) \
X6( marktype, "mark", int dummy2 , ("<mark>")) \
X6( integertype, "integer", int i, ("%d",o.i)) \
X6( booleantype, "boolean", bool b, (o.b?"true":"false")) \
X6( realtype, "real", float f, ("%f",o.f)) \
X6( nametype, "name", int n, ("%s%s", \
(o.flags & Fxflag)?"":"/", names[o.n])) \
X6( stringtype, "string", char *s, ("%s",o.s)) \
X6( filetype, "file", FILE *file, ("<file %p>",(void *)o.file)) \
X6( arraytype, "array", Object *a, ("<array %u>",o.length)) \
X6( dicttype, "dict", struct s_pair *d, ("<dict %u>",o.length)) \
X6(operatortype, "operator", void (*o)(), ("<op>")) \
#define X6(a, b, c, d) #a,
char *typestring[] = { OBJECT_TYPES };
#undef X6
// the Object type
//forward reference so s_object can contain s_objects
typedef struct s_object Object;
// the s_object structure:
// a bit convoluted, but it boils down to four members:
// type, flags, length, and payload (union of type-specific data)
// the first named union member is integer, so a simple literal object
// can be created on the fly:
// Object o = {integertype,0,0,4028}; //create an int object, value: 4028
// Object nl = {nulltype,0,0,0};
struct s_object {
#define X6(a, b, c, d) a,
enum e_type { OBJECT_TYPES } type;
#undef X6
unsigned int flags;
#define Fread 1
#define Fwrite 2
#define Fexec 4
#define Fxflag 8
size_t length; //for lint, was: unsigned int
#define X6(a, b, c, d) c;
union { OBJECT_TYPES };
#undef X6
};
Одной большой проблемой были строки формата printf. Хотя он выглядит круто, он просто фокусируется. Поскольку он используется только в одной функции, чрезмерное использование макроса фактически разделяет информацию, которая должна быть вместе; и это делает функцию нечитаемой сама по себе. Обфускация вдвойне неудачна в функции отладки, подобной этой.
//print the object using the type format specifier from the macro
//used by O_equal (ps: =) and O_equalequal (ps: ==)
void printobject(Object o) {
switch (o.type) {
#define X6(a, b, c, d) \
case a: printf d; break;
OBJECT_TYPES
#undef X6
}
}
Так что не увлекайтесь. Как и я.
Ответ 3
В виртуальной машине Oracle HotSpot для языка программирования Java® существует файл globals.hpp
, который использует RUNTIME_FLAGS
таким образом.
Смотрите исходный код:
Ответ 4
Мне нравится использовать макросы X для создания "богатых перечислений", которые поддерживают итерацию значений enum, а также получение строкового представления для каждого значения перечисления:
#define MOUSE_BUTTONS \
X(LeftButton, 1) \
X(MiddleButton, 2) \
X(RightButton, 4)
struct MouseButton {
enum Value {
None = 0
#define X(name, value) ,name = value
MOUSE_BUTTONS
#undef X
};
static const int *values() {
static const int a[] = {
None,
#define X(name, value) name,
MOUSE_BUTTONS
#undef X
-1
};
return a;
}
static const char *valueAsString( Value v ) {
#define X(name, value) static const char str_##name[] = #name;
MOUSE_BUTTONS
#undef X
switch ( v ) {
case None: return "None";
#define X(name, value) case name: return str_##name;
MOUSE_BUTTONS
#undef X
}
return 0;
}
};
Это не только определяет перечисление MouseButton::Value
, но и позволяет мне делать такие вещи, как
// Print names of all supported mouse buttons
for ( const int *mb = MouseButton::values(); *mb != -1; ++mb ) {
std::cout << MouseButton::valueAsString( (MouseButton::Value)*mb ) << "\n";
}
Ответ 5
Я использую довольно массивный X-macro для загрузки содержимого INI файла в структуру конфигурации, среди прочего, вращающуюся вокруг этой структуры.
Вот что выглядит мой файл "configuration.def":
#define NMB_DUMMY(...) X(__VA_ARGS__)
#define NMB_INT_DEFS \
TEXT("long int") , long , , , GetLongValue , _ttol , NMB_SECT , SetLongValue ,
#define NMB_STR_DEFS NMB_STR_DEFS__(TEXT("string"))
#define NMB_PATH_DEFS NMB_STR_DEFS__(TEXT("path"))
#define NMB_STR_DEFS__(ATYPE) \
ATYPE , basic_string<TCHAR>* , new basic_string<TCHAR>\
, delete , GetValue , , NMB_SECT , SetValue , *
/* X-macro starts here */
#define NMB_SECT "server"
NMB_DUMMY(ip,TEXT("Slave IP."),TEXT("10.11.180.102"),NMB_STR_DEFS)
NMB_DUMMY(port,TEXT("Slave portti."),TEXT("502"),NMB_STR_DEFS)
NMB_DUMMY(slaveid,TEXT("Slave protocol ID."),0xff,NMB_INT_DEFS)
.
. /* And so on for about 40 items. */
Это немного запутанно, признаюсь. Быстро становится ясно, что я действительно не хочу писать все эти объявления типов после каждого макроса поля. (Не волнуйтесь, есть большой комментарий, чтобы объяснить все, что я пропустил для краткости.)
И вот как я объявляю конфигурацию struct:
typedef struct {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) TYPE ID;
#include "configuration.def"
#undef X
basic_string<TCHAR>* ini_path; //Where all the other stuff gets read.
long verbosity; //Used only by console writing functions.
} Config;
Затем, в коде, сначала значения по умолчанию считываются в конфигурационную структуру:
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,...) \
conf->ID = CONSTRUCTOR(DEFVAL);
#include "configuration.def"
#undef X
Затем INI считывается в конфигурационную структуру следующим образом, используя библиотеку SimpleIni:
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,DEREF...)\
DESTRUCTOR (conf->ID);\
conf->ID = CONSTRUCTOR( ini.GETTER(TEXT(SECT),TEXT(#ID),DEFVAL,FALSE) );\
LOG3A(<< left << setw(13) << TEXT(#ID) << TEXT(": ") << left << setw(30)\
<< DEREF conf->ID << TEXT(" (") << DEFVAL << TEXT(").") );
#include "configuration.def"
#undef X
И переопределения флагов командной строки, которые также форматируются с одинаковыми именами (в длинной форме GNU), применяются в методе foillowing, используя библиотеку SimpleOpt:
enum optflags {
#define X(ID,...) ID,
#include "configuration.def"
#undef X
};
CSimpleOpt::SOption sopt[] = {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) {ID,TEXT("--") #ID TEXT("="), SO_REQ_CMB},
#include "configuration.def"
#undef X
SO_END_OF_OPTIONS
};
CSimpleOpt ops(argc,argv,sopt,SO_O_NOERR);
while(ops.Next()){
switch(ops.OptionId()){
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,...) \
case ID:\
DESTRUCTOR (conf->ID);\
conf->ID = STRCONV( CONSTRUCTOR ( ops.OptionArg() ) );\
LOG3A(<< TEXT("Omitted ")<<left<<setw(13)<<TEXT(#ID)<<TEXT(" : ")<<conf->ID<<TEXT(" ."));\
break;
#include "configuration.def"
#undef X
}
}
И так далее, я также использую один и тот же макрос для вывода вывода -help -flag и примера файла ini по умолчанию, config.def включен 8 раз в мою программу. Может быть, "квадратная привязка в круглое отверстие"; как бы на самом деле компетентный программист исходил из этого? Много и много циклов и обработки строк?
Ответ 6
https://github.com/whunmr/DataEx
Я использую следующий xmacros для генерации класса C++ со встроенными функциями сериализации и десериализации.
#define __FIELDS_OF_DataWithNested(_) \
_(1, a, int ) \
_(2, x, DataX) \
_(3, b, int ) \
_(4, c, char ) \
_(5, d, __array(char, 3)) \
_(6, e, string) \
_(7, f, bool)
DEF_DATA(DataWithNested);
Использование:
TEST_F(t, DataWithNested_should_able_to_encode_struct_with_nested_struct) {
DataWithNested xn;
xn.a = 0xCAFEBABE;
xn.x.a = 0x12345678;
xn.x.b = 0x11223344;
xn.b = 0xDEADBEEF;
xn.c = 0x45;
memcpy(&xn.d, "XYZ", strlen("XYZ"));
char buf_with_zero[] = {0x11, 0x22, 0x00, 0x00, 0x33};
xn.e = string(buf_with_zero, sizeof(buf_with_zero));
xn.f = true;
__encode(DataWithNested, xn, buf_);
char expected[] = { 0x01, 0x04, 0x00, 0xBE, 0xBA, 0xFE, 0xCA,
0x02, 0x0E, 0x00 /*T and L of nested X*/,
0x01, 0x04, 0x00, 0x78, 0x56, 0x34, 0x12,
0x02, 0x04, 0x00, 0x44, 0x33, 0x22, 0x11,
0x03, 0x04, 0x00, 0xEF, 0xBE, 0xAD, 0xDE,
0x04, 0x01, 0x00, 0x45,
0x05, 0x03, 0x00, 'X', 'Y', 'Z',
0x06, 0x05, 0x00, 0x11, 0x22, 0x00, 0x00, 0x33,
0x07, 0x01, 0x00, 0x01};
EXPECT_TRUE(ArraysMatch(expected, buf_));
}
Кроме того, другой пример находится в https://github.com/whunmr/msgrpc.