文本替换宏
预处理器支持文本宏替换和仿函数文本宏替换。
语法
#define 标识符 替换列表 (可选)
|
(1) | ||||||||
#define 标识符 ( 形参 ) 替换列表 (可选)
|
(2) | ||||||||
#define 标识符 ( 形参 , ...) 替换列表 (可选)
|
(3) | (C++11 起) | |||||||
#define 标识符 (...) 替换列表 (可选)
|
(4) | (C++11 起) | |||||||
#undef 标识符
|
(5) | ||||||||
解释
#define 指令
#define 指令将标识符 定义为宏(macro),即指示编译器以将在它之后出现的所有标识符 都替换为替换列表,而它也可以被进一步处理。如果该标识符已被定义为任何类型的宏,那么除非这些定义都相同,否则程序非良构。
仿对象宏
仿对象宏(object-like macro)以替换列表 替换每次出现的被定义标识符。#define 指令的版本 (1) 准确表现如此。
仿函数宏
仿函数宏(function-like macro)以替换列表 替换每次出现的被定义标识符,可选地接受一定量的实参,它们随即替换掉替换列表 中出现的任何对应的形参。
仿函数宏语法类似函数调用语法:每个宏名实例后随一个 ( 作为下个预处理记号,所引入的记号序列将被替换为替换列表。该序列以匹配的 ) 记号终止,跳过中间的匹配左右括号对。
对于版本 (2),实参数量必须与宏定义中的形参数量相同。对于版本 (3,4),实参数量必须多于 (C++20 前)不少于 (C++20 起)形参数量(不计 ...
)。否则程序非良构。如果标识符未使用函数写法,即它自身之后没有括号,那么它完全不被替换。
#define 指令的版本 (2) 定义简单仿函数宏。
#define 指令的版本 (3) 定义有可变数量实参的仿函数宏。额外的实参(是谓可变实参)可用 __VA_ARGS__
标识符访问,它会被与要被替换的标识符一起提供的实参替换。
#define 指令的版本 (4) 定义有可变数量实参的仿函数宏,但无常规实参。额外的实参(是谓可变实参)只能用 __VA_ARGS__
标识符访问,它会被与要被替换的标识符一起提供的实参替换。
对于版本 (3,4),替换列表 可以含有记号序列“ #define F(...) f(0 __VA_OPT__(,) __VA_ARGS__) #define G(X, ...) f(0, X __VA_OPT__(,) __VA_ARGS__) #define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ }) F(a, b, c) // 替换为 f(0, a, b, c) F() // 替换为 f(0) G(a, b, c) // 替换为 f(0, a, b, c) G(a, ) // 替换为 f(0, a) G(a) // 替换为 f(0, a) SDEF(foo); // 替换为 S foo; SDEF(bar, 1, 2); // 替换为 S bar = { 1, 2 }; |
(C++20 起) |
注意:如果仿函数宏的实参中包含不为匹配的左右括号对所保护的逗号(最常出现于模板实参列表中,如 assert(std::is_same_v<int, int>); 或 BOOST_FOREACH(std::pair<int, int> p, m)),那么逗号被解释成宏实参分隔符,并造成由于实参数量不匹配所致的编译失败。
保留宏名
包含标准库标头的翻译单元不可 #define 或 #undef 在任何标准库标头中声明的名字。
使用标准库任何部分的翻译单元不可 #define 或 #undef 词法上等同于下列内容的名称:
(C++11 起) |
否则,行为未定义。
# 与 ## 运算符
仿函数宏中,如果替换列表 中一个标识符前有 # 运算符,那么该标识符在运行形参替换的基础上以引号包围,实际上创建一个字符串字面量。另外,预处理器为内嵌的字符串字面量(如果存在)外围的引号添加反斜杠以进行转义,并按需要双写字符串中的反斜杠。移除所有前导和尾随空白符,并将文本中间(除内嵌字符串字面量中间外)的任何空白符序列缩减成单个空格。此操作被称为“字符串化”,如果字符串化的结果不是合法的字符串字面量,那么行为未定义。
# 出现于 #define showlist(...) puts(#__VA_ARGS__) showlist(); // 展开成 puts("") showlist(1, "x", int); // 展开成 puts("1, \"x\", int") |
(C++11 起) |
如果替换列表 中任何两个相继标识符之间有 ## 运算符,那么这两个标识符(首先未被宏展开)在运行形参替换的基础上将结果进行拼接。此操作被称为“拼接”或“记号粘贴”。只有一同组成合法记号的记号才可以粘贴:如组成更长标识符的标识符、组成数字的数字位,或组成 +=
的运算符 +
和 =
。不能通过粘贴 /
和 *
来创建注释,这是因为注释在考虑文本宏替换前就已经被移除了。如果连接的结果不是合法记号,那么行为未定义。
注意:一些编译器提供了一项扩展,允许 ## 在逗号后及 __VA_ARGS__
前出现,此情况下 ## 在存在可变实参时不做任何事,但在不存在可变实参时移除逗号:这使得可以定义如 fprintf (stderr, format, ##__VA_ARGS__) 这样的宏。也可以通过标准方式使用 __VA_OPT__
实现,例如 fprintf (stderr, format __VA_OPT__(, ) __VA_ARGS__)。 (C++20 起)
#undef 指令
#undef 指令取消定义标识符,即取消 #define 指令所作的标识符 定义。如果标识符未关联到宏,那么忽略该指令。
预定义宏
下列宏名已预定义于每个翻译单元中:
__cplusplus |
代表所用的 C++ 标准版本,展开成值
|
__STDC_HOSTED__ (C++11) |
如果实现有宿主(在操作系统下运行)则展开成整数常量 1,如果实现自立(不在操作系统下运行)则展开成 0 (宏常量) |
__FILE__ |
展开成当前文件名,作为字符串字面量,可用 #line 指令更改 (宏常量) |
__LINE__ |
展开成源文件行号,整数常量,可用 #line 指令更改 (宏常量) |
__DATE__ |
展开成翻译日期,形式为 "Mmm dd yyyy" 的字符串。如果月中日期数小于 10 则 "dd" 的首字符为空格。月份名如同以 std::asctime() 生成 (宏常量) |
__TIME__ |
展开成翻译时间,形式为 "hh:mm:ss" 的字符串字面量 (宏常量) |
__STDCPP_DEFAULT_NEW_ALIGNMENT__ (C++17) |
展开成 std::size_t 字面量,其值为对不具对齐的 operator new 的调用所保证的对齐(更大的对齐将传递给具对齐重载,如 operator new(std::size_t, std::align_val_t)) (宏常量) |
__STDCPP_BFLOAT16_T____STDCPP_FLOAT16_T____STDCPP_FLOAT32_T____STDCPP_FLOAT64_T____STDCPP_FLOAT128_T__ (C++23) |
当且仅当实现支持对应的扩展浮点类型时展开成 1 (宏常量) |
实现可能会预定义下列其他的宏名:
__STDC__ |
如果存在则为实现定义值,典型地用于指示 C 遵从性 (宏常量) | ||||
__STDC_VERSION__ (C++11) |
如果存在则为实现定义值 (宏常量) | ||||
__STDC_ISO_10646__ (C++11) |
(宏常量) | ||||
__STDC_MB_MIGHT_NEQ_WC__ (C++11) |
如果对于基本字符集成员 'x' == L'x' 可能为假,则展开成 1,如在基于 EBCDIC 并且为 wchar_t 使用 Unicode 的系统上。 (宏常量) | ||||
__STDCPP_THREADS__ (C++11) |
如果程序能拥有多于一个执行线程则展开成 1 (宏常量) |
__STDCPP_STRICT_POINTER_SAFETY__ (C++11)(C++23 中移除) |
如果实现支持严格 std::pointer_safety 则展开成 1 (宏常量) |
这些宏的值(除了 __FILE__
和 __LINE__
)在整个翻译单元保持为常量。试图重定义或取消定义这些宏会导致未定义行为。
注意:在每个函数体的作用域内部都有一个名为 __func__ 的特殊的函数局域预定义变量,它被定义为一个持有具有实现定义格式的函数名的静态字符数组。它不是预处理器宏,但它与 |
(C++11 起) |
语言功能特性检测宏标准定义一组对应于 C++11 或之后引入的 C++ 语言功能特性的预处理器宏。它们被用来以简单且可移植的方式检测所说的特性是否存在。 详见功能特性测试。 |
(C++20 起) |
示例
#include <iostream> // 制造函数工厂并使用它 #define FUNCTION(name, a) int fun_##name() { return a; } FUNCTION(abcd, 12) FUNCTION(fff, 2) FUNCTION(qqq, 23) #undef FUNCTION #define FUNCTION 34 #define OUTPUT(a) std::cout << "output: " #a << '\n' // 在后面的宏定义中使用之前的宏 #define WORD "Hello " #define OUTER(...) WORD #__VA_ARGS__ int main() { std::cout << "abcd: " << fun_abcd() << '\n'; std::cout << "fff: " << fun_fff() << '\n'; std::cout << "qqq: " << fun_qqq() << '\n'; std::cout << FUNCTION << '\n'; OUTPUT(million); // 注意这里没有引号 std::cout << OUTER(World) << '\n'; std::cout << OUTER(WORD World) << '\n'; }
输出:
abcd: 12 fff: 2 qqq: 23 34 output: million Hello World Hello WORD World
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
LWG 294 | C++98 | 包含了某个标准库标头的翻译单元可以包含定义了在另一个标准库标头中声明的名字的宏 | 已禁止 |
P2621R2 | C++23 | 不允许通过拼接记号来组成通用字符名 | 允许 |