预处理器
预处理器在编译前的翻译阶段 4 执行。预处理的结果是单个文件,接下来它会被传递给实际编译器。
1、指令:
预处理指令控制预处理器的行为。每个指令占据一行并拥有下列格式:
# 字符
预处理指令(define、undef、include、if、ifdef、ifndef、else、elif、endif、line、error、pragma 之一)[1]
实参(取决于指令)
换行符
允许空指令(即在 # 后直接换行),而它没有效果。
导入声明(<import>)也是预处理指令。(C++20 起)
预处理指令不得来自宏展开。
#define EMPTY
EMPTY # include <file.h> // 不是预处理指令
2、能力:
1、预处理器有能力翻译源文件:
1.1、有条件编译源文件的某些部分(由 #if、#ifdef、#ifndef、#else、#elif 和 #endif 指令控制)。
1.2、替换文本宏,同时可能对标识符进行拼接或加引号(由 #define 和 #undef 指令与 # 和 ## 运算符控制)。
1.3、包含其他文件(由 #include 指令控制并以 __has_include 检查 (C++17 起))。
1.4、引发错误(由 #error 指令控制)
2、能控制预处理器的下列方面:
2.1、由实现定义的行为(由 #pragma 指令和 _Pragma 运算符 (C++11 起)控制)。
2.2、对预处理器可用的文件名和行信息(由 #line 指令控制)
3、脚注
1、↑ 这些指令是标准定义的。标准不定义其他指令的行为:它们可以被忽略、拥有一些有用的含义或导致编译时错误。即使忽略,也会在预处理器完成工作时将它们从源码中移除。一种常用的非标准扩展是 #warning 指令,它在编译期间放出一条用户定义的消息。
参阅
4、条件包含
预处理器支持有条件地编译源文件的某些部分。这一行为由 #if、#else、#elif、#ifdef、#ifndef 与 #endif 指令所控制。
1、语法:
#if 表达式
#ifdef 标识符
#ifndef 标识符
#elif 表达式
#else
#endif
2、解释:
条件预处理块由 #if、#ifdef 或 #ifndef 指令开始,然后可选地包含任意多个 #elif 指令,接下来是最多一个可选的 #else 指令,并以 #endif 指令结束。嵌套的条件预处理块会被单独处理。
各个 #if、#elif、#else、#ifdef 和 #ifndef 指令所控制的代码块在第一个不属于内部嵌套的条件预处理块的 #elif、#else 或 #endif 指令处结束。
#if、#ifdef 和 #ifndef 指令测试其所指定的条件(见下文),如果条件求值为真,则编译其控制的代码块。此时后续的 #else 和 #elif 指令将被忽略。否则,如果所指定的条件求值为假,则跳过其所控制的代码块,然后处理后续的 #else 或 #elif 指令(如果存在)。前一种情况下,#else 指令所控制的代码块将会无条件地进行编译。后一种情况下,#elif 指令按照与 #if 指令相同的方式执行:即测试条件是否满足,并根据其结果决定编译或跳过其所控制的代码块,并在后一种情况下继续处理后续的 #elif 和 #else 指令。条件预处理块以 #endif 指令结束。
3、条件的求值:
1、#if, #elif
表达式 是常量表达式。
1、表达式可含有形如“defined 标识符”或“defined (标识符)”的一元运算符。当此 标识符 已经被定义为宏名,或者此 标识符 为 __has_include (C++17 起)时结果为 1,否则结果为 0。
2、在进行所有宏展开和 defined 及 __has_include (C++17 起) 表达式的求值后,任何非布尔字面量的标识符都被替换成数字 0(这包含词法上为关键字的标识符,但不包括如 and 之类的代用记号)。
3、当 表达式 求值为非零值时,包含其所控制的代码块,否则跳过该代码块。
4、预处理器语境中, __has_cpp_attribute 表达式检测给定属性记号是否受支持及其受支持版本。见属性测试。(C++20 起)
注: CWG 1955 前,“#if cond1 ... #elif cond2”和“#if cond1 ... #else”后面跟着“#if cond3”是不同的,因为当 cond1 为真时第二个 #if 将被跳过,且 cond3 并不需要是良构的,而 #elif 中的 cond2 则必须是合法的表达式。 CWG 1955 开始,引领跳过的代码块的 #elif 也被跳过。
2、#ifdef, #ifndef
检查标识符是否被定义为宏名。
“#ifdef 标识符”与“#if defined 标识符”实质上等价。
“#ifndef 标识符”与“#if !defined 标识符”实质上等价。
示例:
#define ABCD 2
#include <iostream>
int main()
{
#ifdef ABCD
std::cout << "1: yes\n";
#else
std::cout << "1: no\n";
#endif
#ifndef ABCD
std::cout << "2: no1\n";
#elif ABCD == 2
std::cout << "2: yes\n";
#else
std::cout << "2: no2\n";
#endif
#if !defined(DCBA) && (ABCD < 2*4-3)
std::cout << "3: yes\n";
#endif
}
输出:
1: yes
2: yes
3: yes
3、缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR 应用于 出版时的行为 正确行为
CWG 1955 C++14 要求失败的 #elif 跳过失败的 #elif
中的表达式合法
参阅
4、错误指令
显示给定的错误消息并使程序非良构。
1、语法
#error 错误消息
2、解释
遇到 #error 指令后,实现显示诊断消息 错误消息,并使程序非良构(编译停止)。
错误消息可由多个词组成,不必在引号中。
参阅
5、实现定义的行为控制:
实现定义的行为受 #pragma 指令控制。
1、语法:
#pragma 语用形参 (1)
_Pragma ( 字符串字面量 ) (2) (C++11 起)
1) 具有由实现定义的行为方式
2) 从 字符串字面量 移除 L 前缀(若存在),外层引号,及前导/尾随空白符,将每个 \" 替换为 "、将每个 \\ 替换为 \,然后将结果记号化(如翻译阶段 3 中一样),再如同将结果作为 (1) 中的输入来予以使用。
2、解释
pragma 指令控制编译器的特定于实现的行为,如禁用编译器警告或更改对齐要求。无法识别的语用会被忽略。
3、非标准语用
ISO C++ 语言标准并不要求编译器支持任何语用。不过,多种实现都支持几种非标准的语用:
1、#pragma STDC
ISO C 语言标准要求 C 编译器支持下列三种语用,一些 C++ 编译器供应商在它们的 C++ 前端中以不同的程度支持它们:
#pragma STDC FENV_ACCESS 实参 (1)
#pragma STDC FP_CONTRACT 实参 (2)
#pragma STDC CX_LIMITED_RANGE 实参 (3)
其中 实参 是 ON、OFF 和 DEFAULT 之一。
1) 若设为 ON,则告知编译器,程序将访问或修改浮点环境,这意味着可能推翻标志测试和模式更改(例如,全局共用子表达式删除,代码移动,及常量折叠)的优化将被禁用。默认值由实现定义,通常是 OFF。
2) 允许浮点表达式的缩略(contracting)行为,即一种优化,表达式严格按书面求值时可被观察到的一些舍入错误和浮点异常将被忽略。例如,允许 (x*y) + z 使用单条融合乘加 CPU 指令实现。默认值由实现定义,通常是 ON。
3) 告知编译器,复数的乘法、除法和绝对值可以使用简化的数学公式 (x+iy)×(u+iv) = (xu-yv)+i(yu+xv) (x+iy)/(u+iv) = [(xu+yv)+i(yu-xv)]/(u2+v2),和 |x+iy| = √x2+y2,而不考虑中间溢出的可能性。换言之,程序员保证传递给这些函数的值范围是受限的。默认值为 OFF。
如果上述三种语用的任一条并非出现于所有外部声明之外,或所有显式声明或复合语句中的所有语句之前,则程序的行为未定义。
注意:不支持这些 pragma 的编译器可能提供等价的编译时选项,例如 gcc 的 -fcx-limited-range 和 -ffp-contract。
2、#pragma once
#pragma once 是受到绝大多数现代编译器支持的非标准语用。当某个头文件中包含它时,指示编译器只对其分析一次,即使它在同一源文件中(直接或间接)被包含了多次也是如此。
阻止同一头文件的多次包含的标准方式是使用包含防护:
#ifndef LIBRARY_FILENAME_H
#define LIBRARY_FILENAME_H
// 头文件的内容
#endif /* LIBRARY_FILENAME_H */
从而在任何翻译单元中,该头文件除首次以外被包含时都被排除出编译。所有现代编译器都记录头文件使用了包含防护的事实,只要该防护仍有定义,再遇到该头文件时就不再分析它。(例子见 gcc )
使用 #pragma once 时,同一个头文件可以变为
#pragma once
// 头文件的内容
不同于头文件防护,这条语用使得错误地在多个文件中使用相同的宏名变得不可能。另一方面,因为带 #pragma once 的文件是基于其文件系统层次的身份所排除的,所以若头文件在项目中有多个位置,则这不能防止包含它两次。
3、#pragma pack
此 pragma 族控制后继定义的类和联合体的最大对齐。
#pragma pack(实参) (1)
#pragma pack() (2)
#pragma pack(push) (3)
#pragma pack(push, 实参) (4)
#pragma pack(pop) (5)
其中 arg 实参是小的 2 的幂,指定以字节计的新对齐。
1) 设置当前对齐为值 实参 。
2) 设置当前对齐为默认值(由命令行选项指定)。
3) 推入当前对齐的值到内部栈。
4) 推入当前对齐的值到内部栈然后设置当前对齐为值 实参 。
5) 从内部栈弹出顶条目然后设置(恢复)当前对齐为该值。
#pragma pack 可以指定类的对齐,然而它不能使类过对齐。
参阅 GCC 与 MSVC 的特定细节。
本节未完成
原因:解释此语用在数据成员的效果还有使用它们的优势与劣势。参考源:
Stack Overflow
本节未完成
原因:暂无示例
4、参阅
外部链接
Visual Studio 2019 的 C++ pragma
GCC 接受的 Pragma
IBM AIX XL C 16.1 的单独 pragma 描述及标准 pragma
Sun Studio 11 C++ 用户指南中的 Appendix B. Pragmas
Intel C++ compiler pragmas
HP aCC A.06.25 的发行注记(包括 pragma)
6、源文件包含:
将其他源文件包含到当前源文件中紧随指令之后的一行。
1、语法
#include <文件名> (1)
#include "文件名" (2)
__has_include ( " 文件名 " )
__has_include ( < 文件名 > ) (3) (C++17 起)
任何预处理记号(宏常量或表达式)都可以作为给 #include 和 __has_include (C++17 起)的实参,只要它们能展开成 < > 或 " " 所环绕的字符序列即可。
2、解释
1,2) 将 文件名 所标识的源文件包含到当前源文件中紧随指令后的一行。找不到文件的情况下,程序非良构。
1) 以由实现定义的方式搜索文件。此语法的意图是搜索由实现所掌控的文件。典型实现仅搜索标准包含目录。这些标准包含目录中隐含地包含标准 C++ 库和标准 C 库。用户通常能通过编译器选项来控制标准包含目录。
2) 以由实现定义的方式搜索文件。此语法的意图是搜索不被实现所掌控的文件。典型实现首先于当前文件所在的目录搜索,然后仅当找不到文件时,才在 (1) 中的标准包含目录搜索。
3) 预处理器常量表达式,若找到文件名则求值为 1,而若找不到则求值为 0。若其实参不是给 #include 指令的有效实参,则程序非良构。
3、注解
包含一个文件时,它将经过翻译阶段 1-4 的处理,这可能递归地包含嵌套 #include 指令的展开。为避免(可能传递性地)重复包含相同文件,和由文件包含自身造成的无限递归,通常使用头文件防护:整个头文件被包装在下列结构中:
#ifndef FOO_H_INCLUDED /* 任何唯一地映射到文件的名称 */
#define FOO_H_INCLUDED
// 文件内容在此
#endif
许多编译器亦实现有类似效果的非标准 pragma #pragma once:若已包含了同一文件(这里以特定于 OS 的方式确定文件的身份),则禁止处理该文件。
__has_include 结果为 1 仅表明存在有指定名称的头或源文件。它并不意味着包含该头或源文件时不会导致错误,或它会包含任何有意义的内容。例如在同时支持 C++14 和 C++17 模式(并在其 C++14 模式作为一项遵从标准的扩展而提供 __has_include)的 C++ 实现上,__has_include(<optional>) 在 C++14 模式中可为 1,但实际上 #include <optional> 可能导致错误。
4、示例
#if __has_include(<optional>)
# include <optional>
# define have_optional 1
namespace guard { using std::optional; }
#elif __has_include(<experimental/optional>)
# include <experimental/optional>
# define have_optional 1
# define experimental_optional 1
namespace guard { using std::experimental::optional; }
#else
# define have_optional 0
#endif
#include <iostream>
int main()
{
if (have_optional)
std::cout << "<optional> 存在。\n";
int x = 42;
#if have_optional == 1
guard::optional<int> i = x;
#else
int* i = &x;
#endif
std::cout << "i = " << *i << '\n';
}
可能的输出:
<optional> 存在。
i = 42
5、参阅
cpp/header C++ 标准库头文件列表
7、文件名和行信息:
更改预处理器中当前的行号和文件名。
1、语法
#line 行号 (1)
#line 行号 "文件名" (2)
2、解释
1) 将当前的预处理器行号更改为 行号。宏 __LINE__ 在该点后的展开将产生 行号 加上自此遇到的实际代码行数。
2) 还将当前的预处理器文件名更改为 文件名。宏 __FILE__ 在该点后的展开将生成 文件名。
任何预处理器记号(宏常量或表达式)都允许作为 #line 的实参,只要它们展开成合法的十进制整数,可选地后随一个合法的字符串即可。
行号 必须是至少有一个十进制位的序列(否则程序非良构),并且始终按十进制解释(即使它以 0 开始也是如此)。
若 行号 为 0 或大于 32767 (C++11 前)2147483647 (C++11 起),则行为未定义。
3、注解
一些自动代码生成工具通过以其他语言编写的文件产生 C++ 源文件时,会使用此指令。这种情况下,它们在所生成的 C++ 文件中插入 #line 指令,以指代原(人类编辑的)源文件的行号和文件名。
4、示例
#include <cassert>
#define FNAME "test.cc"
int main()
{
#line 777 FNAME
assert(2+2 == 5);
}
输出:
test: test.cc:777: int main(): Assertion `2+2 == 5' failed.
参阅
8、文本替换宏
预处理器支持文本宏替换和仿函数文本宏替换。
1、语法
#define 标识符 替换列表(可选) (1)
#define 标识符( 形参 ) 替换列表(可选) (2)
#define 标识符( 形参, ... ) 替换列表(可选) (3) (C++11 起)
#define 标识符( ... ) 替换列表(可选) (4) (C++11 起)
#undef 标识符 (5)
2、解释
1、#define 指令
#define 指令将 标识符 定义为宏,即指示编译器以将其后出现的所有 标识符 都替换为 替换列表,而它也可以被进一步处理。若该标识符已被定义为任何类型的宏,则除非这些定义都相同,否则程序非良构。
2、仿对象宏
仿对象宏(object-like)以 替换列表 替换每次出现的被定义 标识符。#define 指令的版本 (1) 准确表现如此。
3、仿函数宏
仿函数宏(function-like)以 替换列表 替换每次出现的被定义 标识符,可选地接受一定量的实参,它们随即替换掉 替换列表 中出现的任何对应的 形参。
仿函数宏语法类似函数调用语法:每个宏名实例后随一个 ( 作为下个预处理记号,所引入的记号序列将被替换为 替换列表。该序列以匹配的 ) 记号终止,跳过中间的匹配左右括号对。
对于版本 (2),实参数量必须与宏定义中的形参数量相同。对于版本 (3,4) ,实参数量必须多于 (C++20 前)不少于 (C++20 起)形参数量(不计 ...)。否则程序非良构。若标识符未使用函数写法,即它自身之后无括号,则它完全不被替换。
#define 指令的版本 (2) 定义简单仿函数宏。
#define 指令的版本 (3) 定义有可变数量实参的仿函数宏。额外的实参(是谓可变实参)可用 __VA_ARGS__ 标识符访问,它会被与要被替换的标识符一起提供的实参替换。
#define 指令的版本 (4) 定义有可变数量实参的仿函数宏,但无常规实参。额外的实参(是谓可变实参)只能用 __VA_ARGS__ 标识符访问,它会被与要被替换的标识符一起提供的实参替换。
对于版本 (3,4),替换列表 可以含有记号序列“__VA_OPT__ ( 内容 )”,若 __VA_ARGS__ 非空,则它会被 内容 替换,否则不展开成任何内容。
#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)),则逗号被解释成宏实参分隔符,并造成由于实参数量不匹配所致的编译失败。
4、保留宏名
包含标准库头文件的翻译单元不可 #define 或 #undef 声明于任何标准库头文件中所声明的名字。
使用标准库任何部分的翻译单元不可 #define 或 #undef 词法上等同于下列内容的名称:
关键字
有特殊含义的标识符
任何标准属性记号。
(C++11 起)
除了可定义 likely 与 unlikely 为仿函数宏。(C++20 起)
否则,行为未定义。
5、# 与 ## 运算符
仿函数宏中,如果替换列表 中一个标识符前有 # 运算符,则该标识符在运行形参替换的基础上以引号包围,实际上创建一个字符串字面量。另外,预处理器为内嵌的字符串字面量(若它存在)外围的引号添加反斜杠以进行转义,并按需要双写字符串中的反斜杠。移除所有前导和尾随空白符,并将文本中间(除内嵌字符串字面量中间外)的任何空白符序列缩减成单个空格。此操作被称为“字符串化”,若字符串化的结果不是合法的字符串字面量,则行为未定义。
# 出现于 __VA_ARGS__ 之前时,展开后的 __VA_ARGS__ 整体被包在引号中:
#define showlist(...) puts(#__VA_ARGS__)
showlist(); // 展开成 puts("")
showlist(1, "x", int); // 展开成 puts("1, \"x\", int")
(C++11 起)
如果 替换列表 中任何两个相继标识符之间有 ## 运算符,则这两个个标识符(首先未被宏展开)在运行形参替换的基础上将结果进行拼接。此操作被称为“拼接”或“记号粘贴”。只有一同组成合法记号的记号才可以粘贴:如组成更长标识符的标识符、组成数字的数字位,或组成 += 的运算符 + 和 =。不能通过粘贴 / 和 * 来创建注释,这是因为注释在考虑文本宏替换前就已经被移除了。如果连接的结果不是合法记号,则行为未定义。
注意:一些编译器提供了一项扩展,允许 ## 出现于逗号后及 __VA_ARGS__ 前,此情况下 ## 在存在可变实参时不做任何事,但在不存在可变实参时移除逗号:这使得可以定义如 fprintf (stderr, format, ##__VA_ARGS__) 这样的宏。
6、#undef 指令
#undef 指令取消定义 标识符,即取消 #define 指令所作的 标识符 定义。如果标识符未关联到宏,则忽略该指令。
7、预定义宏
下列宏名已预定义于每个翻译单元中。
__cplusplus: 代表所用的 C++ 标准版本,展开成值 199711L(C++11 前) 、 201103L(C++11) 、 201402L(C++14) 、 201703L(C++17) 或 202002L(C++20)
(宏常量)
__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))
(宏常量)
实现可能预定义下列其他的宏名。
__STDC__: 如果存在则为实现定义值,典型地用于指示 C 遵从性
(宏常量)
__STDC_VERSION__(C++11): 如果存在则为实现定义值
(宏常量)
__STDC_ISO_10646__(C++11): 如果 wchar_t 使用 Unicode ,则展开成 yyyymmL 形式的整数常量,日期指示所支持的 Unicode 的最近版本
(宏常量)
__STDC_MB_MIGHT_NEQ_WC__(C++11): 如果对于基本字符集成员 'x' == L'x' 可能为假,则展开成 1,如在基于 EBCDIC 并且为 wchar_t 使用 Unicode 的系统上。
(宏常量)
__STDCPP_STRICT_POINTER_SAFETY__(C++11): 如果实现支持严格 std::pointer_safety 则展开成 1
(宏常量)
__STDCPP_THREADS__(C++11): 如果程序能拥有多于一个执行线程则展开成 1
(宏常量)
这些宏的值(除了 __FILE__ 和 __LINE__)在整个翻译单元保持为常量。试图重定义或取消定义这些宏会导致未定义行为。
注意:在每个函数体的作用域内部都有一个名为 __func__ 的特殊的函数局域预定义变量,它被定义为一个持有具有实现定义格式的函数名的静态字符数组。它不是预处理器宏,但它与 __FILE__ 和 __LINE__ 一起使用,例如 assert。(C++11 起)
8、语言功能特性测试宏
标准定义一组对应于 C++11 或之后引入的 C++ 语言功能特性的预处理器宏。它们被用来以简单且可移植的方式检测所说的特性是否存在。
详见功能特性测试 。(C++20 起)
9、示例
#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
参阅