X-Macros的实际使用


Answers:


94

几年前,当我开始在代码中使用函数指针时,我发现了X-macros。我是嵌入式程序员,并且经常使用状态机。我经常会这样写代码:

问题是我认为必须维护函数指针表的顺序以使其与我的状态枚举的顺序匹配非常容易出错。

我的一个朋友向我介绍了X-宏,就像灯泡在我的头上掉了一样。严重的是,您一生都在哪里X-宏!

现在,我定义下表:

我可以如下使用它:

作为奖励,我还可以让预处理器按如下方式构建函数原型:

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

但是,我最喜欢的用法是涉及通信处理程序

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

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

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

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

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

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

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

我可以确定有多少个命令,如下所示:

注意:我从未实际实例化offset_struct_t,我只是将其用作编译器为我生成命令数量定义的一种方式。

请注意,然后我可以如下生成函数指针表:

而我的函数原型:

现在,对于有史以来最酷的用途,我可以让编译器计算我的传输缓冲区应该有多大。

同样,此联合类似于我的偏移量结构,它没有实例化,而是可以使用sizeof运算符声明我的传输缓冲区大小。

现在,我的发送缓冲区tx_buf是最佳大小,当我向该comms处理程序添加命令时,我的缓冲区将始终是最佳大小。凉!

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

代替

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

其中offsetof是在“ stddef.h”中定义的标准库宏

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

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


2
哇!我谦卑地接受了这个优越的答案。(但您应该考虑“用户宏”样式:无需取消定义任何内容,无需记住内部的“变量”名称。)
luser droog 2012年

1
非常感谢,今天学到了一些新东西。现在,代替我所有的#define和#undef,我可以执行以下操作:REGISTERTABLE(AS_DECLARATION)REGISTERTABLE(AS_INITIALIZER)非常酷!
ACRL '02

6
“说真的,你一辈子都在哪里X宏!” 潜伏在地狱中,最有可能等待一些毫无戒心的程序员召唤他们。在现代C语言中,您可以像这样在跳转表和枚举之间创建直接的紧密耦合p_func_t jumptable[] = { [STATE0] = func0, [STATE1] = func1 };。注意[]数组大小。现在,要确保没有任何项目丢失,请添加一个编译时检查:_Static_assert(NUM_STATES == sizeof jumptable/sizeof *jumptable, "error");。输入安全,可读,看不到单个宏的类型。
伦丁

2
我的意思是,x宏应该是最后的选择,而不是在遇到程序设计问题时想到的第一件事。
伦丁


38

X-Macros本质上是参数化的模板。因此,如果您需要多种形式的类似物品,它们是完成工作的正确工具。它们允许您创建一个抽象形式并根据不同的规则实例化它。

我使用X宏将枚举值输出为字符串。而且由于遇到了这种情况,我强烈希望这种形式将“用户”宏应用于每个元素。使用多个文件只是要痛苦得多。

我还将它们用于基于对象类型的函数分派。再次劫持我用于创建枚举值的相同宏。

使用宏保证了我所有的数组索引都将与关联的枚举值匹配,因为它们使用宏定义(TYPES宏)中的裸令牌构造了各种形式。

实际上,以这种方式使用X宏有助于编译器提供有用的错误消息。我从上面省略了evalarray函数,因为它会分散我的注意力。但是,如果您尝试编译以上代码(注释掉其他函数调用,并为上下文提供伪typedef),则编译器会抱怨缺少函数。对于我添加的每种新类型,都提醒我在重新编译此模块时添加一个处理程序。因此,X宏有助于确保即使项目不断发展,并行结构也能保持完整。

编辑:

这个答案使我的声誉提高了50%。所以这里还有更多。以下是一个否定示例,回答了这个问题:什么时候使用X-Macros?

此示例显示了将任意代码片段打包到X-“记录”中。我最终放弃了该项目的这个分支,并且在以后的设计中也不使用这种策略(并且不需要尝试)。不知何故,它变得很古怪。确实,该宏被命名为X6,因为在某一点上有6个参数,但是我对更改宏名称感到厌倦。

一个大问题是printf格式字符串。虽然看起来很酷,但这只是焦点。由于仅在一个函数中使用,因此宏的过度使用实际上将应该在一起的信息分开了。并且使该功能本身不可读。在这样的调试功能中,混淆是令人遗憾的。

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


1
我一直在研究一些不同的库来处理C语言中的“对象”,例如Cello和GObject,但它们对我的口味来说都花了很多时间。.另一方面,本文和您的Github代码-很棒的东西,感谢您的灵感。:)
Christoffer Bubach '20

很高兴听到。我也研究了这些内容,并查看了Lisp 1.1手册。我制作的最新一组对象是用于解析器组合器的。我那里的GC非常小而简单。一定要让我知道您要建造什么。这种东西总是看起来很酷。:)
luser droog

8

热门项目和大型项目在X-Macros上的一些实际用法:

Java热点

在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。这使代码难以导航,因此增加了整个代码库的混淆性。



您可以包括其中一些代码吗?当前,这实际上是仅链接的答案。
TankorSmash

5

我喜欢使用X宏来创建“丰富的枚举”,该枚举支持迭代枚举值以及获取每个枚举值的字符串表示形式:

这不仅定义了一个MouseButton::Value枚举,还使我可以执行以下操作


4

我使用了相当大的X宏将INI文件的内容加载到配置结构中,其中包括围绕该结构的其他内容。

这是我的“ configuration.def”文件的样子:

我承认这有点令人困惑。很快变得很清楚,我实际上并不想在每个字段宏之后都写所有这些类型声明。(不用担心,这里有一个很大的评论可以解释我为简洁起见省略的所有内容。)

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

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

然后,使用库SimpleIni将INI如下读取到配置结构中:

命令行标志的覆盖也使用相同的名称(GNU长格式)格式化,它们使用库SimpleOpt以下面的方式如下应用:

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


1

https://github.com/whunmr/DataEx

我正在使用以下xmacros生成具有内置的序列化和反序列化功能的C ++类。

用法:

另外,另一个示例在https://github.com/whunmr/msgrpc中


0

铬在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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.