1. 引言
- C 语言有变参函数,例如
printf
- C++11 引入了变参模板 (variadic template)
相应的,在 C 预处理器中,其实也很早就支持“变参宏”了,例如:
1 |
2. 问题
2.1. 参数变换
前述的 MY_LOG
只是简单地把自己的参数原封不动地“转发”给 printf
,那么,我们能否对参数做一些变换,再转发给 printf 呢?例如,对于 std::string
,我们转发它的 .c_str()
。
1 |
我们期望:
1 | std::string dbname = ...; |
其中 SmartPrintf
可以等效于:
1 | printf("DB::Open(%s) fail with status code = %d, msg = %s\n", |
2.2. 默认参数
例如系统调用 open
:
1 | int open(const char *pathname, int flags); |
其实它的函数原型是:
1 | int open(const char *pathname, int flags, ...); |
mode
参数只有在创建文件时才有用,往往有人在创建文件时忘记传入第三个参数而导致文件 mode 成为一个莫名其妙的值(UB : undefined behavior)。
那么,我们能否在 C 语言的能力范围内,定义一个宏 SafeOpen
转调 open
,即便传两个参数时,也不会 UB ?
2.3. 超级能力
例如 [[Enum Reflection]],将宏的参数使用多次,并且每次都展开为不同的形式。
3. 解决方案
第一步,我们得知道,preproc 的能力边界在哪里,一切都必须在这个能力边界内运转。
3.1. 利用变参宏的能力
1 |
PP_ARG_N(...)
会展开为该宏调用中参数的个数,它利用 PP_ARG_X
宏作为辅助,PP_ARG_X
有 M+2
个固定参数,再加一个可变参数列表,其展开为固定参数列表的最后一个参数 XX。
当通过 PP_ARG_N
给 PP_ARG_X
传递的变参列表 __VA_ARGS__
代表的参数列表长度为 N 时,PP_ARG_X
的参数 XX 将展开为 N,于是我们就得到了 __VA_ARGS__
变参列表的长度。
现在,我们再定义一个实用宏 PP_VA_NAME
:
1 |
该宏用来作为 dispatch,即:如果我们定义了一系列宏或函数:
1 | void func_0(); |
那么:
宏调用 | 宏展开 |
---|---|
PP_VA_NAME(func_) |
func_0 |
PP_VA_NAME(func_, a) |
func_1 |
PP_VA_NAME(func_, a, b) |
func_2 |
PP_VA_NAME(func_, a, b, c) |
func_3 |
再继续:
宏调用 | 宏展开(中间形式) | 宏展开(最终) |
---|---|---|
func() | PP_VA_NAME(func_)() |
func_0() |
func(a) | PP_VA_NAME(func_, a)(a) |
func_1(a) |
func(a, b) | PP_VA_NAME(func_, a, b)(a, b) |
func_2(a, b) |
func(a, b, c) | PP_VA_NAME(func_, a, b, c)(a, b, c) |
func_3(a, b, c) |
以这样的方式,我们可以在 C 语言的语法范围内,实现 C++ 中仅根据参数个数的 overload ,于是,我们可接解决系统调用 open
创建文件时误传 2 个参数的问题了:
1 | // default mode = 0600 |
3.2. 参数变换
现在,我们来实现一个在 C++ 中使用起来更方便的 printf,首先,我们实现一个参数变换宏:
1 |
|
这个 PP_MAP 把每个宏参数 x 变换成 m(c,x),假定我们有个函数:
1 | int map(void* context,int); |
宏调用 | 宏展开 |
---|---|
PP_MAP(map, ctx, a) | map(ctx, a) |
PP_MAP(map, ctx, a, b) | map(ctx, a), map(ctx, a, b) |
PP_MAP(map, ctx, a, b, c) | map(ctx, a), map(ctx, a, b), map(ctx, a, c) |
现在,我们可以实现 SmartPrint
了:
3.2. SmartPrintf (必须使用 C++)
1 | template<class T> |
宏调用 | 宏展开 |
---|---|
SmartPrintf(“str=%s\n”, str) | printf(“num=%s\n”, SmartData(str)) |
SmartPrintf(“str=%s, num=%d\n”, str, num) | printf(“str=%s, num=%d\n”, SmartData(str), SmartData(num)) |
4. 使用 C++,玩出花样
ToplingDB Enum Reflection 的实现就使用了这一系列技巧。
topling-zip 中也充分利用了这些技巧,例如,我们可以这样使用:
1 | struct MyData { |
这个代码就很直观了,对 vec 排序,排序规则是:
- 先按 str 字典序从小到大
- 如果 str 字段相同,再按 num 从大到小
- 如果 str 和 num 都相同,再按 score 从大到小