实际使用X-Macros

我刚刚了解到X-Macros 。 你看过X-Macros的真实用途了吗? 他们什么时候是正确的工具?

几年前,当我开始在代码中使用函数指针时,我发现了Xmacros。 我是一个embedded式程序员,我经常使用状态机。 通常我会写这样的代码:

/* 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-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 }; 

作为奖励,我也可以让预处理器构build我的函数原型如下:

 #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 

然而,我最喜欢的用法是谈到通讯处理程序

首先我创build一个包含每个命令名和代码的通讯表:

 #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 {...}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 }; 

我可以确定有多less个命令如下:

 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,我只是用它作为编译器为我生成的一些命令定义的一种方式。

请注意,我可以生成我的function指针表如下:

 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处理程序,我的缓冲区将永远是最佳的大小。 凉!

另一个用途是创build偏移量表:由于内存通常是embedded式系统的限制,因此当我的跳转表(每个指针X 2个可能的命令2个字节)是一个稀疏数组时,我不想使用512个字节。 相反,我会为每个可能的命令提供一个8位偏移表。 这个偏移量然后用于索引到我的实际跳转表,现在只需要NUM_COMMANDS * sizeof(指针)。 在我的情况下,定义了10个命令。 我的跳转表是20字节长,我有一个长度为256个字节的偏移量表,总共是276字节,而不是512字节。 然后我打电话给我的function如下:

 jump_table[offset_table[command]](); 

代替

 jump_table[command](); 

我可以像这样创build一个偏移表:

 /* 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”中定义的标准库macros

作为一个好处,有一个非常简单的方法来确定是否支持命令代码:

 bool command_is_valid(uint8_t command) { /* return false if not valid, or true (non 0) if valid */ return offset_table[command]; } 

这也是为什么在我的COMMAND_TABLE I保留命令字节0.我可以创build一个名为“process_reserved()”的函数,如果使用任何无效的命令字节索引到我的偏移表将被调用。

Xmacros本质上是参数化的模板。 所以如果你需要几个类似的东西,他们是这个工作的正确工具。 它们允许您创build一个抽象表单并根据不同的规则进行实例化。

我使用Xmacros来输出枚举值作为string。 而且由于遇到它,我非常喜欢这个forms,它需要一个“用户”macros来应用到每个元素。 包含多个文件比使用更加痛苦。

 /* 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]); */ 

我也使用它们作为基于对象types的函数调度。 再次通过劫持我用来创build枚举值的macros。

 #define TYPES(_) \ _(invalid) \ _(null) \ _(mark) \ _(integer) \ _(real) \ _(array) \ _(dict) \ _(save) \ _(name) \ _(string) \ /*enddef TYPES */ #define AS_TYPE(_) _ ## type , enum { TYPES(AS_TYPE) }; 

使用macros保证我所有的数组索引都将匹配相关的枚举值,因为它们使用来自macros定义(TYPESmacros)的裸示例构造它们的各种forms。

 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); } 

通过这种方式使用Xmacros实际上可以帮助编译器提供有用的错误消息。 我从上面省略了evalarray函数,因为它会分散我的观点。 但是,如果你试图编译上面的代码(当然注释掉其他的函数调用,并且提供了一个用于上下文的虚拟typedef),那么编译器会抱怨缺less的函数。 对于我添加的每个新types,我都会提醒在我重新编译此模块时添加一个处理程序。 所以X-macro有助于保证即使在项目不断增长的情况下,并行结构也能保持不变。

编辑:

这个答案使我的声望提高了50%。 所以这里有一点。 下面是一个反面的例子 ,回答这个问题: 什么时候使用X-Macros?

这个例子显示了将任意代码片段打包到X-“logging”中。 我最终放弃了这个项目的分支,并没有在后来的devise中使用这个策略(而不是为了尝试)。 不知何故,它变得不自由。 事实上这个macros命名为X6,因为有一次有6个参数,但是我已经厌倦了改变macros命名。

 /* Object types */ /* "'X'" macros for Object type definitions, declarations and initializers */ // abcd // 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",oi)) \ X6( booleantype, "boolean", bool b, (ob?"true":"false")) \ X6( realtype, "real", float f, ("%f",of)) \ X6( nametype, "name", int n, ("%s%s", \ (o.flags & Fxflag)?"":"/", names[on])) \ X6( stringtype, "string", char *s, ("%s",os)) \ 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格式string。 虽然它看起来很酷,但它只是一个焦点。 由于它只用于一个function,过度使用macros应该是实际分离的信息, 它使得这个函数本身是不可读的。 在这样的debuggingfunction中混淆是非常不幸的。

 //print the object using the type's 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 } } 

所以不要被带走 像我一样。

在用于Java®编程语言的Oracle HotSpot虚拟机中,有文件globals.hpp ,以这种方式使用globals.hpp

查看源代码:

  • JDK 7
  • JDK 8
  • JDK 9

我喜欢使用Xmacros来创build“丰富的枚举”,它支持迭代枚举值以及获取每个枚举值的string表示forms:

 #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"; } 

我使用了一个相当庞大的Xmacros来将INI文件的内容加载到configuration结构中,除此之外还围绕着这个结构。

这就是我的“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. */ 

我承认这有点令人困惑。 很快我们就明白,我实际上并不想在每个字段macros之后写出所有这些types的声明。 (别担心,为了简洁起见,我有一个很大的解释。

这就是我如何声明configuration结构:

 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; 

然后,在代码中,首先将默认值读入configuration结构中:

 #define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,...) \ conf->ID = CONSTRUCTOR(DEFVAL); #include "configuration.def" #undef X 

然后,使用库SimpleIni将INI读入configuration结构如下:

 #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长格式),使用库SimpleOpt以foowowing方式应用如下:

 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 } } 

依此类推,我也使用相同的macros来打印–help -flag输出和样本默认的ini文件,在我的程序中包含了8次configuration.def。 也许;“正方形钉入圆孔” 一个真正能胜任的程序员如何进行这个工作呢? 很多很多的循环和string处理?

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.xa = 0x12345678; xn.xb = 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