败犬のC++专题 宏魔法篇
1. 前置知识
本文假设你已经了解:
- 定义和使用宏。
#ifdef
等条件编译指令。- 预定义宏,如
__FILE__
__LINE__
。 #
##
运算符。- 宏的可变参数
...
和__VA_ARGS__
。
2. 宏展开规则
参考文章:C语言 宏嵌套的展开规则 - 彷徨而立的文章 - 知乎
简单来讲是三条规则:
- 外层宏的定义包含
#
##
运算符,优先展开外层宏。 - 外层宏的定义不包含
#
##
运算符,优先展开内层宏。 - 禁止递归:宏展开后,忽略这个宏。
宏展开规则在 cppref Scanning and Replacement 章节可以验证。
3. 宏展开规则举例:行号 __LINE__
的字符串形式
我们知道 __LINE__
会替换为数字,#
可以转换为字符串,但是不能 #define LINE_STR #__LINE__
(因为 #
运算符只作用于形参)。
于是稍作调整:
#include <iostream>
#define TO_STRING(x) #x
#define LINE_STR TO_STRING(__LINE__)
int main() {
std::cout << LINE_STR << std::endl; // LINE_STR 替换为 "__LINE__"
}
没错这样也不行。根据宏展开规则,LINE_STR
会替换为 TO_STRING(__LINE__)
,由于 TO_STRING
的定义里包含了 #
运算符,所以直接会展开成 "__LINE__"
。
相信大家读到这就会了,那就是套两层,把 #
运算符藏起来就好了:
#include <iostream>
#define TO_STRING_(x) #x
#define TO_STRING(x) TO_STRING_(x)
#define LINE_STR TO_STRING(__LINE__)
int main() {
std::cout << LINE_STR << std::endl; // LINE_STR 替换为 "8"
}
4. 宏展开规则举例:变量名包含 __LINE__
的宏
这个例子也是类似的。直接这么写是不对的:
#define macro(x) int x##__LINE__ = 0
int main() {
macro(x); // 替换为 int x__LINE__ = 0;
}
答案是套两层:
#define CONCAT_(x, y) x##y
#define CONCAT(x, y) CONCAT_(x, y)
#define macro(x) int CONCAT(x, __LINE__) = 0
int main() {
macro(x); // int x6 = 0;
}
5. 使用宏时,参数里有逗号
MACRO(std::array<int, 2>)
,会被当成两个参数 std::array<int
和 2>
。
一般可以套个小括号搞定 MACRO((std::array<int, 2>))
。如果这是最后一个参数也可以修改宏的定义,用变长参数 ...
__VA_ARGS__
。
如果搞不定,就要用 boost 的宏 BOOST_IDENTITY_TYPE
(或者参考它的实现)。
6. 宏魔法之 for
实现 TEST(n, f)
展开为 f(1), f(2), ..., f(n)
。
这个东西用模版做最合适。如果只能用宏,可以用拼接的方式:
#define TEST1(f) f(1)
#define TEST2(f) TEST1(f), f(2)
#define TEST3(f) TEST2(f), f(3)
#define TEST4(f) TEST3(f), f(4)
#define TEST(n, f) TEST##n(f)
extern int f(int);
int g() {
TEST(4, f);
}
还可以借助 boost 实现:https://godbolt.org/z/387nbc419。
7. include 的参数可以是宏
include 的参数可以是宏,可以写成 #include H_XX
然后用编译参数传入 H_XX
宏。
8. 下划线占位符
下划线占位符是 C++26 的语法 https://wg21.link/p2169,一个作用是忽略结构化绑定的部分参数:
void foo() {
int _ = 0;
auto [_, x] = std::make_pair(1, 2);
}
这个功能一定程度上可以用宏代替,没错就是用编译器扩展 __COUNTER__
:
#define CONCAT(l, r) l##r
#define IGNORE(count) CONCAT(IGNORE_, count)
#define _ IGNORE(__COUNTER__)
void foo() {
int _ = 0;
auto [_, x] = std::make_pair(1, 2);
}
9. x-macro 和头文件结合
10. 根据参数个数的分发
C 语言,有没有办法让 f(1)
调用函数 f1
,f(1, 2)
调用函数 f2
呢?有的兄弟,有的。这是一段 linux 的代码:
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _n, X...) _n
#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
虽然有上限,不够完美,但很多时候够了。基于这个和上文提到的 CONCAT,我们可以完成如下操作:
#include <stdio.h>
#define CONCAT_(x, y) x##y
#define CONCAT(x, y) CONCAT_(x, y)
#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, \
_13, _14, _15, _n, ...) \
_n
#define COUNT_ARGS(...) \
__COUNT_ARGS(, ##__VA_ARGS__, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, \
2, 1, 0)
void f1(int a) { printf("f1: %d\n", a); }
void f2(int a, int b) { printf("f2: %d, %d\n", a, b); }
#define f(...) CONCAT(f, COUNT_ARGS(__VA_ARGS__))(__VA_ARGS__)
int main() {
f(1); // 调用 f1(1)
f(1, 2); // 调用 f2(1, 2)
return 0;
}
11. 写在最后
能力有限,只能讲几个宏的简单寄巧。C/C++ 宏编程的艺术 - BOT Man的文章 - 知乎 这篇文章讲到更多魔法,包括符号匹配、数值运算等,感兴趣的可以看看。
切记:“如果能不用宏,就不用宏”原则,除了必要场景,宏和任何其他方案对比,都是 0 优势纯劣势。工程代码尽可能优先用模板,然后是宏,最后是代码生成,从而保证代码的可维护性。