在 C++ 中不建议使用宏,对于原先 C 中大量使用宏的场景,均建议使用其他方式替代,比如使用 const
常量和 enum
枚举量代替宏定义的常量,使用 template
模版函数/模板类替代宏定义的函数段。但某些场景下宏依然是不可替代的,例如代码的条件编译、函数复杂流程控制。本文主要讲述如何在变长参数的宏里确定参数的个数及其应用。
从 C++ 11 开始,宏开始支持变长参数,例如:
#define Array(...) { __VA_ARGS__ }
#define Print(fmt, ...) printf(fmt, ##__VA_ARGS__)
int main() {
int a[] = Array(1, 2, 3); // int a[] = {1, 2, 3};
int b[] = Array(); // int b[] = {};
Print("OK"); // printf("OK");
Print("%d, %d", 1, 2); // printf("%d, %d", 1, 2);
}
点击此处查看在线编译结果。使用 __VA_ARGS__
代替参数列表,和模板中的变长参数 Args...
类似。可以使用 g++ -std=c++11 -E
得到宏展开后的代码。值得注意的是 Print("OK")
会被展开成 printf("OK")
,宏里面 fmt
后面的逗号被自动省略了。这并非是 C++ 标准,而是编译器的特性,gcc 和 clang 均实现了该特性。下文截取自文献1:
Note: some compilers offer an extension that allows ## to appear after a comma and before
__VA_ARGS__
, in which case the ## does nothing when the variable arguments are present, but removes the comma when the variable arguments are not present: this makes it possible to define macros such asfprintf (stderr, format, ##__VA_ARGS__)
.
C++ 标准中并没有直接提供一种方法来获取 __VA_ARGS__
变长参数的长度,但人民的智慧是无穷无尽的,参考文献 2 和文献 3 中就提供了一些奇技淫巧来解决这个问题,简单来说是这样:
#define TENTH(_1, _2, _3, _4, _5, _6, _7, _8, _9, N, ...) N
#define COUNT(...) TENTH(__VA_ARGS__, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
int main() {
COUNT(2);
COUNT(2, 3);
COUNT(2, 3, 3);
}
点击此处查看在线编译结果。首先定义个名为 TENTH
的宏,来获取参数列表中的第十个参数;再定义一个 COUNT
宏,将 __VA_ARGS__
和 9 到 1 连起来之后,返回其第十位。如果 __VA_ARGS__
的长度为 1,那么返回的数值刚好是 1,以此类推。这里的 TENTH
可以按需求改成更长的宏。
但该宏有一个严重的 bug
,即当参数为空时,仍然会返回 1。其原因为:
COUNT()
-> TENTH(, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
-> 1
__VA_ARGS__
虽然为空,但是其后面的逗号却依然会保留,并占据一个参数的位置。如果使用 C++ 20,那么使用 __VA_OPT__
宏便可以解决这个问题,但目前使用 20 是不现实的。对于 C++ 11,如果使用 G++,并使用 -std=gnu++11
标准,则可以通过以下技巧解决该问题:
#define ELEVENTH(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N
#define COUNT(...) ELEVENTH(dummy, ##__VA_ARGS__, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
int main() {
COUNT();
COUNT(2);
COUNT(2, 3);
COUNT(2, 3, 3);
}
点击此处查看在线编译结果。通过 ,##__VA_ARGS__
为空时自动省略逗号的方式,巧妙的规避这个问题。但使用 -std=c++11
时该 trick 失效,可以点击此处查看,故该方法并不能很好的推广使用。
以上解决方案均属于在预处理期间获得参数长度。如果将范围拓展至编译期,那么还可以通过 C++ 的模板方法解决该问题:
#include <cstdio>
#include <tuple>
#define COUNT(...) \
std::tuple_size<decltype(std::make_tuple(__VA_ARGS__))>::value
int main() {
printf("%d\n", COUNT());
printf("%d\n", COUNT(2));
printf("%d\n", COUNT(2, 3));
printf("%d\n", COUNT(2, 3, 3));
}
点击此处查看在线编译结果。
网络上有广泛的关于宏是否是图灵完备的讨论,例如参考文献 4 和参考文献 5。在使用一些技巧、使得宏可以很多次展开的情况下,笔者倾向于认为宏是接近图灵完备的。既然已经接近图灵完备了,那么变长参数为空的问题就一定可以解决。实际上参考文献 6 中确实有解决方案,一个名为 HAS_ARGS
的宏,这里简化如下:
#define CAT(a, b) a##b
#define FIRST(first, ...) first
#define SECOND(first, second, ...) second
#define IS_PROBE(...) SECOND(__VA_ARGS__, 0)
#define PROBE() ~, 1
#define NOT(x) IS_PROBE(CAT(_NOT_, x))
#define _NOT_0 PROBE()
#define BOOL(x) NOT(NOT(x))
#define HAS_ARGS(...) BOOL(FIRST(_END_OF_ARGUMENTS_ __VA_ARGS__)())
#define _END_OF_ARGUMENTS_() 0
int main() {
HAS_ARGS(); // 0
HAS_ARGS(1); // 1
HAS_ARGS(1, 2); // 1
}
点击此处查看在线编译结果。这里用到了共计 10 个宏,非常完美地在预处理期实现了变长参数是否为空的判断。有了这个宏,再加上 IF_ELSE
,就可以实现完美的、C++ 11 兼容的变长参数长度获取:
#define CAT(a, b) a##b
#define FIRST(first, ...) first
#define SECOND(first, second, ...) second
#define IS_PROBE(...) SECOND(__VA_ARGS__, 0)
#define PROBE() ~, 1
#define NOT(x) IS_PROBE(CAT(_NOT_, x))
#define _NOT_0 PROBE()
#define BOOL(x) NOT(NOT(x))
#define HAS_ARGS(...) BOOL(FIRST(_END_OF_ARGUMENTS_ __VA_ARGS__)())
#define _END_OF_ARGUMENTS_() 0
#define IF_ELSE(condition) _IF_ELSE(BOOL(condition))
#define _IF_ELSE(condition) CAT(_IF_, condition)
#define _IF_1(...) __VA_ARGS__ _IF_1_ELSE
#define _IF_0(...) _IF_0_ELSE
#define _IF_1_ELSE(...)
#define _IF_0_ELSE(...) __VA_ARGS__
#define TENTH(_1, _2, _3, _4, _5, _6, _7, _8, _9, N, ...) N
#define COUNT(...) \
IF_ELSE(HAS_ARGS(__VA_ARGS__)) \
(TENTH(__VA_ARGS__, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0))(0)
int main() {
COUNT(); // 0
COUNT(2); // 1
COUNT(2, 3); // 2
COUNT(2, 3, 3); // 3
}
点击此处查看在线编译结果。参考文献 6 中还介绍了宏递归的实现,有兴趣可以继续学习一下。
费劲千辛万苦,获得了宏内变长参数的长度,具体有什么用呢?简单来说,可以通过参数长度的判断,实现宏函数的重载,并且是预处理期的重载。举个例子🌰:
#define CHECK_RET(expr) \
do { \
int _ret = (expr); \
if (_ret != 0) { \
return _ret; \
} \
} while (0)
这是笔者 17 年在一家公司实习时学会的技巧。C++ 很多项目中使用返回码而非异常来进行错误处理,当返回值非 0 时即为出错,很多场景下可以直接返回该错误码,也就是使用 CHECK_RET
宏。如果有需要出错时返回指定值,那么正常来说就需要增加一个新的宏。但使用宏的重载的话,就可以这样实现:
#define CHECK_RET(expr, ...) \
do { \
int _ret = (expr); \
if (_ret != 0) { \
return IF_ELSE(HAS_ARGS(__VA_ARGS__))(__VA_ARGS__)(_ret); \
} \
} while (0)
int main() {
CHECK_RET(1);
// do { int _ret = (1); if (_ret != 0) { return _ret; } } while (0);
CHECK_RET(1, 233);
// do { int _ret = (1); if (_ret != 0) { return 233 ; } } while (0);
}
点击此处查看在线编译结果。当变长参数长度为 0 时,直接返回错误码;当变长参数长度为 1 时,返回指定错误码。