我刚刚了解到 X-Macros .您见过 X-Macros 的哪些实际用途?他们什么时候是完成这项工作的正确工具?
最佳答案
几年前,当我开始在我的代码中使用函数指针时,我发现了 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};
问题是我认为必须维护我的函数指针表的顺序以使其与我的状态枚举的顺序相匹配非常容易出错。
我的一个 friend 向我介绍了 X-macros,它就像一个灯泡在我脑海中熄灭了。说真的,我这辈子你都去哪儿了 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
};
作为奖励,我还可以让预处理器按如下方式构建我的函数原型(prototype):
#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
然而,我最喜欢的用法是在通信处理程序方面
首先我创建一个通讯表,包含每个命令名称和代码:
#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
}
还有我的函数原型(prototype):
#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
同样,这个 union 就像我的偏移量结构,它没有被实例化,相反我可以使用 sizeof 运算符来声明我的传输缓冲区大小。
uint8_t tx_buf[sizeof(tx_buf_t)];
现在我的传输缓冲区 tx_buf 是最佳大小,当我向这个通信处理程序添加命令时,我的缓冲区将始终是最佳大小。酷!
另一个用途是创建偏移量表: 由于内存通常是嵌入式系统的限制条件,因此当它是一个稀疏数组时,我不想为我的跳转表使用 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()”的函数,如果使用任何无效命令字节索引到我的偏移表中,将调用该函数。
关于c - X-Macros 的实际使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6635851/