C++ Macro: Number of Arguments

2019.11.18
SF-Zhou

在 C++ 中不建议使用宏,对于原先 C 中大量使用宏的场景,均建议使用其他方式替代,比如使用 const 常量和 enum 枚举量代替宏定义的常量,使用 template 模版函数/模板类替代宏定义的函数段。但某些场景下宏依然是不可替代的,例如代码的条件编译、函数复杂流程控制。本文主要讲述如何在变长参数的宏里确定参数的个数及其应用。

1. 宏中的变长参数

从 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 as fprintf (stderr, format, ##__VA_ARGS__).

2. 变长参数的长度

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));
}

点击此处查看在线编译结果。

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 中还介绍了宏递归的实现,有兴趣可以继续学习一下。

4. 变长参数的应用

费劲千辛万苦,获得了宏内变长参数的长度,具体有什么用呢?简单来说,可以通过参数长度的判断,实现宏函数的重载,并且是预处理期的重载。举个例子🌰:

#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 时,返回指定错误码。

References

  1. "Replacing text macros", C++ Reference
  2. "C++ preprocessor __VA_ARGS__ number of arguments", Stack Overflow
  3. "Optional Parameters with C++ Macros", Stack Overflow
  4. "Is the C99 preprocessor Turing complete?", Stack Overflow
  5. "C语言的宏是图灵完备的吗?", 知乎
  6. "C Pre-Processor Magic", Jhnet Blog