我刚刚了解了X-Macros。您看到了X-Macros在现实世界中的哪些用途?他们什么时候才是工作的正确工具?
我刚刚了解了X-Macros。您看到了X-Macros在现实世界中的哪些用途?他们什么时候才是工作的正确工具?
Answers:
几年前,当我开始在代码中使用函数指针时,我发现了X-macros。我是嵌入式程序员,并且经常使用状态机。我经常会这样写代码:
/* 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) \
我在表中有大写和小写名称,因为大写将用于枚举,小写用于函数名称。
然后,我还为每个命令定义了结构,以定义每个命令的外观:
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(pointer)。以我为例,定义了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()”的函数,如果使用任何无效的命令字节索引到我的偏移表中,该函数将被调用。
p_func_t jumptable[] = { [STATE0] = func0, [STATE1] = func1 };
。注意[]
数组大小。现在,要确保没有任何项目丢失,请添加一个编译时检查:_Static_assert(NUM_STATES == sizeof jumptable/sizeof *jumptable, "error");
。输入安全,可读,看不到单个宏的类型。
X-Macros本质上是参数化的模板。因此,如果您需要多种形式的类似物品,它们是完成工作的正确工具。它们允许您创建一个抽象形式并根据不同的规则实例化它。
我使用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宏有助于确保即使项目不断发展,并行结构也能保持完整。
编辑:
这个答案使我的声誉提高了50%。所以这里还有更多。以下是一个否定示例,回答了这个问题:什么时候不使用X-Macros?
此示例显示了将任意代码片段打包到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'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
}
}
所以不要被带走。像我一样
热门项目和大型项目在X-Macros上的一些实际用法:
在Java®编程语言的Oracle HotSpot虚拟机中,有一个文件globals.hpp
,该文件RUNTIME_FLAGS
以这种方式使用。
查看源代码:
在网络错误的net_error_list.h列表是这种形式的宏展开的长长的名单:
NET_ERROR(IO_PENDING, -1)
net_errors.h从同一目录使用它:
enum Error {
OK = 0,
#define NET_ERROR(label, value) ERR_ ## label = value,
#include "net/base/net_error_list.h"
#undef NET_ERROR
};
这种预处理器魔术的结果是:
enum Error {
OK = 0,
ERR_IO_PENDING = -1,
};
我不喜欢这种特殊用法,因为常量的名称是通过添加来动态创建的ERR_
。在此示例中,NET_ERROR(IO_PENDING, -100)
定义常量ERR_IO_PENDING
。
使用简单的文字搜索ERR_IO_PENDING
,就无法看到此常量的定义位置。相反,要找到定义,必须搜索IO_PENDING
。这使代码难以导航,因此增加了整个代码库的混淆性。
我喜欢使用X宏来创建“丰富的枚举”,该枚举支持迭代枚举值以及获取每个枚举值的字符串表示形式:
#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";
}
我使用了相当大的X宏将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. */
我承认这有点令人困惑。很快变得很清楚,我实际上并不想在每个字段宏之后都写所有这些类型声明。(不用担心,这里有一个很大的评论可以解释我为简洁起见省略的所有内容。)
这就是我声明配置结构的方式:
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
然后,使用库SimpleIni将INI如下读取到配置结构中:
#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以下面的方式如下应用:
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文件,configuration.def在程序中包含8次。也许是“方形钉入圆孔”;一个真正有能力的程序员将如何进行呢?很多循环和字符串处理?
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中。
铬在dom_code_data.inc上有一个有趣的X宏变体。除了它不仅是一个宏,而且是一个完全独立的文件。该文件用于在不同平台的扫描代码,USB HID代码和类似字符串的名称之间进行键盘输入映射。
该文件包含如下代码:
DOM_CODE_DECLARATION {
// USB evdev XKB Win Mac Code
DOM_CODE(0x000000, 0x0000, 0x0000, 0x0000, 0xffff, NULL, NONE), // Invalid
...
};
每次宏调用实际上都会传递7个参数,并且宏可以选择要使用的参数和要忽略的参数。一种用法是在OS键码与平台无关的扫描码与DOM字符串之间进行映射。在不同的OS上使用不同的宏来选择适合该OS的键码。
// Table of USB codes (equivalent to DomCode values), native scan codes,
// and DOM Level 3 |code| strings.
#if defined(OS_WIN)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
{ usb, win, code }
#elif defined(OS_LINUX)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
{ usb, xkb, code }
#elif defined(OS_MACOSX)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
{ usb, mac, code }
#elif defined(OS_ANDROID)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
{ usb, evdev, code }
#else
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
{ usb, 0, code }
#endif
#define DOM_CODE_DECLARATION const KeycodeMapEntry usb_keycode_map[] =
#include "ui/events/keycodes/dom/dom_code_data.inc"
#undef DOM_CODE
#undef DOM_CODE_DECLARATION