真实使用X-Macros

Agn*_*kas 67 c macros c-preprocessor x-macros

我刚学会了X-Macros.您看过X-Macros的实际用途?他们什么时候成为工作的合适工具?

ACR*_*CRL 88

几年前,当我开始在代码中使用函数指针时,我发现了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};
Run Code Online (Sandbox Code Playgroud)

问题是我认为它非常容易出错,必须维护我的函数指针表的顺序,以便它匹配我的枚举状态的顺序.

我的一个朋友向我介绍了X-macros,就像一个灯泡在我脑海中消失了.说真的,你一生都在哪里x-macros!

所以现在我定义下表:

#define STATE_TABLE \
        ENTRY(STATE0, func0) \
        ENTRY(STATE1, func1) \
        ENTRY(STATE2, func2) \
        ...
        ENTRY(STATEX, funcX) \
Run Code Online (Sandbox Code Playgroud)

我可以使用它如下:

enum
{
#define ENTRY(a,b) a,
    STATE_TABLE
#undef ENTRY
    NUM_STATES
};
Run Code Online (Sandbox Code Playgroud)

p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
    STATE_TABLE
#undef ENTRY
};
Run Code Online (Sandbox Code Playgroud)

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

#define ENTRY(a,b) static void b(void);
    STATE_TABLE
#undef ENTRY
Run Code Online (Sandbox Code Playgroud)

另一种用法是声明和初始化寄存器

#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
Run Code Online (Sandbox Code Playgroud)

然而,我最喜欢的用途是通信处理程序

首先,我创建一个包含每个命令名称和代码的comms表:

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

我在表中有大写和小写的名称,因为大写将用于枚举,小写用于函数名称.

然后我还为每个命令定义结构,以定义每个命令的外观:

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

etc.
Run Code Online (Sandbox Code Playgroud)

同样,我为每个命令响应定义结构:

typedef struct {...}command1_resp_t;
typedef struct {...}command2_resp_t;

etc.
Run Code Online (Sandbox Code Playgroud)

然后我可以定义我的命令代码枚举:

enum
{
#define ENTRY(a,b,c) a##_CMD = c,
    COMMAND_TABLE
#undef ENTRY
};
Run Code Online (Sandbox Code Playgroud)

我可以定义我的命令长度枚举:

enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
    COMMAND_TABLE
#undef ENTRY
};
Run Code Online (Sandbox Code Playgroud)

我可以定义我的响应长度枚举:

enum
{
#define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t);
    COMMAND_TABLE
#undef ENTRY
};
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

注意:我从来没有实际实例化offset_struct_t,我只是用它作为编译器为我生成我的命令数量定义的一种方式.

注意我可以生成我的函数指针表,如下所示:

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

我的函数原型:

#define ENTRY(a,b,c) void process_##b(void);
    COMMAND_TABLE
#undef ENTRY
Run Code Online (Sandbox Code Playgroud)

最后,为了最酷的使用,我可以让编译器计算我的传输缓冲区应该有多大.

/* 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
Run Code Online (Sandbox Code Playgroud)

再次,这个联合就像我的偏移结构,它没有实例化,而是我可以使用sizeof运算符来声明我的传输缓冲区大小.

uint8_t tx_buf[sizeof(tx_buf_t)];
Run Code Online (Sandbox Code Playgroud)

现在我的传输缓冲区tx_buf是最佳大小,当我向这个comms处理程序添加命令时,我的缓冲区将始终是最佳大小.凉!

另一个用途是创建偏移表:由于内存通常是嵌入式系统的约束,因此当我是一个稀疏数组时,我不想为跳转表(每个指针2个字节X 256个可能的命令)使用512个字节.相反,我将为每个可能的命令提供一个8位偏移表.然后使用此偏移量来索引到我的实际跳转表,现在只需要NUM_COMMANDS*sizeof(指针).在我的情况下定义了10个命令.我的跳转表是20字节长,我有一个256字节长的偏移表,总共276字节而不是512字节.然后我调用我的函数:

jump_table[offset_table[command]]();
Run Code Online (Sandbox Code Playgroud)

代替

jump_table[command]();
Run Code Online (Sandbox Code Playgroud)

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

/* 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
Run Code Online (Sandbox Code Playgroud)

其中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];
}
Run Code Online (Sandbox Code Playgroud)

这也是我的COMMAND_TABLE I保留命令字节0的原因.我可以创建一个名为"process_reserved()"的函数,如果使用任何无效的命令字节索引到我的偏移表中,将调用该函数.

  • “说真的,我一生的X宏到哪里去了!” 潜伏在地狱中,最有可能等待一些毫无戒心的程序员召唤他们。在现代C语言中,您可以像这样在跳转表和枚举之间创建直接紧密的耦合:`p_func_t jumptable [] = {[STATE0] = func0,[STATE1] = func1};`。注意数组大小的“ []”。现在要确保没有任何项目丢失,请添加一个编译时检查:`_Static_assert(NUM_STATES == sizeof jumptable / sizeof * jumptable,“ error”);`。输入安全,易读的类型,而不是单个宏。 (4认同)
  • 我的观点是 x 宏应该是_最后的手段_,而不是当您面临一些程序设计问题时首先想到的事情。 (4认同)
  • 哇!我谦卑地接受了这个优越的答案。(但是您应该考虑“用户宏”样式:无需取消定义任何内容,无需记住内部的“变量”名称。) (2认同)

lus*_*oog 35

X-Macros本质上是参数化模板.因此,如果你需要几种类似的东西,它们是适合这项工作的工具.它们允许您创建抽象表单并根据不同的规则对其进行实例化.

我使用X-macro输出枚举值作为字符串.自从遇到它以来,我非常喜欢这种形式,它将"用户"宏应用于每个元素.多文件包含对于使用来说更加痛苦.

/* 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]); */
Run Code Online (Sandbox Code Playgroud)

我也使用它们进行基于对象类型的函数调度.再次通过劫持我用来创建枚举值的相同宏.

#define TYPES(_) \
    _(invalid) \
    _(null) \
    _(mark) \
    _(integer) \
    _(real) \
    _(array) \
    _(dict) \
    _(save) \
    _(name) \
    _(string) \
/*enddef TYPES */

#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };
Run Code Online (Sandbox Code Playgroud)

使用宏保证我的所有数组索引都将匹配关联的枚举值,因为它们使用宏定义中的裸令牌(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);
}
Run Code Online (Sandbox Code Playgroud)

以这种方式使用X-macro实际上可以帮助编译器提供有用的错误消息.我从上面省略了evalarray函数,因为它会分散我的注意力.但是,如果你试图编译上面的代码(注释时的其它函数调用,并提供一个虚拟的typedef上下文,当然),编译器会抱怨缺少的功能.对于我添加的每个新类型,我提醒在重新编译此模块时添加一个处理程序.因此,即使项目增长,X-macro也有助于保证并行结构保持完整.

编辑:

这个答案使我的声誉提高了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
};
Run Code Online (Sandbox Code Playgroud)

一个大问题是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
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Rol*_*lig 7

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

查看源代码:


Fre*_*abe 5

我喜欢使用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;
  }
};
Run Code Online (Sandbox Code Playgroud)

这不仅定义了一个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";
}
Run Code Online (Sandbox Code Playgroud)


VIT*_*MAN 5

我使用一个非常庞大的 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. */
Run Code Online (Sandbox Code Playgroud)

这有点令人困惑,我承认。很明显,我实际上并不想在每个字段宏之后编写所有这些类型声明。(别担心,有一个很大的注释可以解释我为了简洁而省略的所有内容。)

这就是我声明配置结构的方式:

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;
Run Code Online (Sandbox Code Playgroud)

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

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

然后,使用库 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
Run Code Online (Sandbox Code Playgroud)

来自命令行标志的覆盖,也使用相同的名称(以 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
    }
  }
Run Code Online (Sandbox Code Playgroud)

依此类推,我也使用相同的宏来打印 --help -flag 输出和示例默认 ini 文件,configuration.def 在我的程序中包含 8 次。“方钉入圆孔”,也许;一个真正有能力的程序员将如何处理这个问题?大量的循环和字符串处理?