在 C 代码中,宏定义中可以进行"文本"层面上的变换,比如在 systemd 的链表实现 (类似 Linux kernel 的链表) 中有这样的用法,
#define LIST_FIELDS(t,name) \ t *name##_next, *name##_prev"##" 能完成所谓的 Token Concatenation, 具体解释看 gnu.org 的手册, 和维基百科,简单的说,## 能把前后两个 token 连为一个。上边定义的 LIST_FIELDS 可以这样使用,
struct Job { LIST_FIELDS(Job, transaction); LIST_FIELDS(Job, run_queue); }编译的时候就能扩展为
struct Job { Job *transaction_next, *transaction_prev; Job *run_queue_next, *run_queue_prev; }这样确实很聪明,也很省事。但是如果你不熟悉这种用法,初读代码会一头雾水,因为遇到下边这样的代码时,根本找不到这些 _next 和 _prev 变量的定义,无论是用 cscope 还是 grep。
assert(!j->transaction_next); assert(!j->transaction_prev);
1. 没有定义的 to_string 和 from_string 函数
最近看 systemd 代码,遇到了一个叫 log_target_from_string 的函数。某个头文件有它的声明,
LogTarget log_target_from_string(const char *s);但是却找不到它的定义。最后发现是这样的,
/* src/log.c */ static const char *const log_target_table[] = { [LOG_TARGET_CONSOLE] = "console", [LOG_TARGET_SYSLOG] = "syslog", [LOG_TARGET_KMSG] = "kmsg", [LOG_TARGET_SYSLOG_OR_KMSG] = "syslog-or-kmsg", [LOG_TARGET_NULL] = "null", [LOG_TARGET_AUTO] = "auto" }; DEFINE_STRING_TABLE_LOOKUP(log_target, LogTarget); /* src/util.h */ #define DEFINE_STRING_TABLE_LOOKUP(name,type) \ const char *name##_to_string(type i) { \ if (i < 0 || i >= (type) ELEMENTSOF(name##_table)) \ return NULL; \ return name##_table[i]; \ } \ type name##_from_string(const char *s) { \ type i; \ unsigned u = 0; \ assert(s); \ for (i = 0; i < (type)ELEMENTSOF(name##_table); i++) \ if (name##_table[i] && \ streq(name##_table[i], s)) \ return i; \ if (safe_atou(s, &u) >= 0 && \ u < ELEMENTSOF(name##_table)) \ return (type) u; \ return (type) -1; \ } \ struct __useless_struct_to_allow_trailing_semicolon__也就是说有一个通用的宏 DEFINE_STRING_TABLE_LOOKUP,它提供了 to_string 和 from_string 这两个函数的“模板”。
systemd 需要进行很多的 enum 数值和对应的字符串之间的转换,这些对应关系都是定义在一个个的字符串数组中,比如上面的 log_target_table。转换操作其实很简单,无非就是在数组中进行查找,但是代码树里有很多这样的数组,也有很多的 enum 类型。如果要给每个数组都定义自己的 to_string 和 from_string 函数,那肯定是一大坨重复代码。
使用 token concatenation 显然是一个很聪明的办法,达到的效果跟 C++ 的模板机制是类似的,能够大幅度地减少代码的重复。DRY! (C vs. C++... 不敢妄下评论,you be the judge)。
(定义一个通用的查找函数也许是另一种办法)。