字符串化操作符 #

有时候我们希望能够将参数转换为字符串进行处理,#可以将一个 token 字符串化,示例如下:

1
2
3
4
5
6
7
8
9
10
11
#define WARN_IF(EXP) \
if (EXP) \
{\
printf("Warning: " #EXP "\n");\
}

string x = "0";
WARN_IF(x == "0");

void* p = nullptr;
WARN_IF(p == nullptr);

需要注意的三点:

  1. #操作符并不是简单的添加双引号,它会自动对特殊字符进行转义
  2. # 操作符只能对 func-like 的参数使用
  3. 由于参数会转换为 token 列表,所以前后的空白符都会被忽略,中间的空白符会被压缩为 1 个,注释会被忽略并变成一个空白符。

token粘贴操作符 ##

合并新的token可以用来提供动态生成token的能力:

1
2
3
4
#define GETTER(x, T) T get_##x() { return this->x; }

GETTER(foo, const int)
-> const int get_foo() { return this->x; }

obj-like的递归展开

在替换列表中出现的宏会被展开,这一过程将递归的进行下去,且是深度优先的,示例如下:

1
2
3
4
5
6
7
8
9
10
#define foo foz bar
#define bar 123
#define foz baz
#define baz 1

foo
-> foz bar
-> baz bar
-> 1 bar
-> 1 123

可以看到当一个宏完全展开后,下一个宏才会被展开,但是如果只有这一条规则,很容易出现无限递归的情况,示例如下:

1
2
3
4
5
6
7
#define foo bar
#define bar foo

foo
-> bar
-> foo
-> 无限循环

标准中对这种情况作了限制,如下所示:

  1. 在展开的过程中,如果替换列表中出现了被展开宏,那么该展开宏不会被展开
  2. 更进一步,在展开的过程中,任何嵌套的展开过程中出现了被展开宏,该被展开宏也不会被展开

func-like的宏展开

  1. identifier-list也就是参数列表里的参数会被完全展开,但如果该参数在替换列表中被#或##所调用,那么该参数不展开;
  2. 使用展开后的结果替换列表中的相关内容;
  3. 执行#和##的结果,并替换相关内容;
  4. 将得到的新的替换列表重新扫描找到可替换的宏名并展开。

下例中的CAT中的参数FOO_由于在替换列表中被##所调用,所以并不会被展开,而是直接合并成一个token FOO_1,并且在重新扫描的阶段被展开为1,在该宏中实际上进行了两次扫描展开:

1
2
3
4
5
6
7
#define FOO_ 0
#define FOO_1 1
#define CAT(x, y) x ## y

CAT(FOO_, 1)
-> FOO_1
-> 1

如果我们希望先展开参数,然后再进行拼接呢?需要借助一个额外的宏来间接做这件事:

1
2
3
4
5
6
7
8
#define FOO_ 0
#define FOO_1 1
#define PRIMITIVE_CAT(x, y) x ## y
#define CAT(x, y) PRIMITIVE_CAT(x, y)

CAT(FOO_, 1)
-> PRIMITIVE_CAT(0, 1)
-> 01

自指的蓝色集合

在第一次FOO展开的时候,当前的蓝色集合为{BAZ}。

首先先完全展开参数,此时展开第一个参数BAZ时,由于此时BAZ已在蓝色集合中,所以停止展开,第二个FOO不在蓝色集合中,因此可以展开:

1
2
3
4
5
6
7
8
#define FOO(x, y) x + y
#define BAR(y) 13 + y
#define BAZ(z) FOO(BAZ(16), FOO(11, 0)) + z

BAZ(15)
-> FOO(BAZ(16), FOO(11, 0)) + 15
-> FOO(BAZ(16), 11 + 0) + 15
-> BAZ(16) + 11 + 0 + 15

括号表达式

将参数用括号括起来使用,这样利用func-like的宏不接括号不会被展开的特性,可以完成一些有意思的东西,示例如下:

1
2
3
4
5
6
7
8
9
#define EXPAND_IF_PAREN(x) EXPAND x
#define EAT(x)

EXPAND_IF_PAREN((12))
-> EAT (12)
->

EXPAND_IF_PAREN(12)
-> EAT 12

检测

1
2
3
4
5
6
7
8
9
10
11
12
#define GET_SEC(x, n, ...) n
#define CHECK(...) GET_SEC(__VA_ARGS__, 0)
#define PROBE(x) x, 1

CHECK(PROBE())
-> CHECK(x, 1,)
-> GET_SEC(x, 1, 0)
-> 1

CHECK(sth_not_empty)
-> GET_SEC(sth_not_empty, 0)
-> 0

__VA_ARGS__

__VA_ARGS__ 是一个可变参数的宏,替换为宏定义中参数列表的最后一个参数为省略号。

##__VA_ARGS__ 宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的”,”去掉的作用,否则会编译出错。

一般用在调试信息的输出中:

1
2
3
4
5
6
7
#define my_print1(...)  printf(__VA_ARGS__)
#define my_print2(fmt,...) printf(fmt,__VA_ARGS__)
#define my_print3(fmt,...) printf(fmt,##__VA_ARGS__)

my_print1("i=%d,j=%d\n",i,j); // 正确
my_print2("iiiiiii\n"); // 编译失败,因为扩展出来只有一个参数,至少要两个及以上参数
my_print3("iiiiiii\n"); // 正确
1
2
3
#define MODULE_NAME "MY_LIBS"
#define error_printf(fmt,...)\
printf("[ERROR]["MODULE_NAME"](%s|%d) "fmt,__func__,__LINE__,##__VA_ARGS__)