Namespaces
Variants

Replacing text macros

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

预处理器支持文本宏替换。同时也支持函数式文本宏替换。

目录

语法

#define 标识符 替换列表  (可选) (1)
#define 标识符  ( 参数  ) 替换列表  (可选) (2)
#define 标识符  ( 参数  , ...) 替换列表  (可选) (3) (C++11 起)
#define 标识符  (...) 替换列表  (可选) (4) (C++11 起)
#undef 标识符 (5)

说明

#define 指令

#define 指令将 identifier 定义为宏,即指示编译器将后续出现的绝大多数 identifier 替换为 replacement-list ,该替换列表还会被进一步处理。例外情况遵循 扫描与替换规则 。若该标识符已被定义为任何类型的宏,则程序非良构,除非宏定义完全相同。

对象式宏

对象式宏会将所有出现的已定义 标识符 替换为 替换列表 。版本(1)的 #define 指令的行为正是如此。

函数式宏

类函数宏会将每个出现的已定义 标识符 替换为 替换列表 ,同时接收若干参数,这些参数随后会替换 替换列表 中对应的 形参 出现位置。

类函数宏调用的语法与函数调用语法相似:每个宏名称实例后紧跟 ( 作为下一个预处理标记时,会引入被 替换序列 所取代的标记序列。该序列由匹配的 ) 标记终止,处理过程中会跳过中间成对出现的左右括号。

对于版本(2),实参数量必须与宏定义中的形参数量相同。对于版本(3,4),实参数量不得少于形参数量( (since C++20) 计算 ... )。否则程序非良构。若标识符不处于函数表示法中,即自身后未跟随括号,则完全不会被替换。

版本(2)的 #define 指令定义了一个简单的函数式宏。

版本 (3) 的 #define 指令定义了一个可变参数函数式宏。额外的参数(称为 可变参数 )可通过 __VA_ARGS__ 标识符访问,该标识符随后会被替换为与该标识符一同提供的参数。

版本 (4) 的 #define 指令定义了一个类似函数的宏,该宏具有可变数量的参数,但没有常规参数。这些参数(称为 可变参数 )只能通过 __VA_ARGS__ 标识符访问,该标识符随后会被替换为提供给待替换标识符的参数。

对于版本(3,4), 替换列表 可包含词法序列 __VA_OPT__( 内容  ) ,当 __VA_ARGS__ 非空时该序列被替换为 内容 ,否则展开为空。

#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)
F(a, b, c) // replaced by f(0, a, b, c)
F()        // replaced by f(0)
#define G(X, ...) f(0, X __VA_OPT__(,) __VA_ARGS__)
G(a, b, c) // replaced by f(0, a, b, c)
G(a, )     // replaced by f(0, a)
G(a)       // replaced by f(0, a)
#define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ })
SDEF(foo);       // replaced by S foo;
SDEF(bar, 1, 2); // replaced by S bar = { 1, 2 };
(since C++20)

注意:如果函数式宏的实参包含未被匹配的左右括号保护的逗号(最常见于模板实参列表,如 assert ( std:: is_same_v < int , int > ) ; BOOST_FOREACH ( std:: pair < int , int > p, m ) ),该逗号会被解释为宏参数分隔符,导致因参数数量不匹配而引发编译失败。

扫描与替换
  • 扫描过程会记录已替换的宏。若扫描发现与这些宏匹配的文本,会将其标记为"待忽略"(所有扫描操作都会忽略它)。这可以防止递归展开。
  • 若扫描发现函数式宏,在将参数放入 替换列表 前会先进行参数扫描。但 # ## 运算符会直接获取未经扫描的参数。
  • 宏完成替换后,会对结果文本重新进行扫描。

注意,可以定义伪递归宏:

#define EMPTY
#define SCAN(x)     x
#define EXAMPLE_()  EXAMPLE
#define EXAMPLE(n)  EXAMPLE_ EMPTY()(n-1) (n)
EXAMPLE(5)
SCAN(EXAMPLE(5))

输出:

EXAMPLE_ ()(5 -1) (5)
EXAMPLE_ ()(5 -1 -1) (5 -1) (5)

保留宏名称

一个 包含标准库头文件 的翻译单元不得对任何 标准库头文件 中声明的名称进行 #define #undef 操作。

使用标准库任何部分的翻译单元不允许 #define #undef 在词法上与下列名称相同的标识符:

(自 C++11 起)

否则,行为是未定义的。

# ## 运算符

在函数式宏中, # 运算符置于 替换列表 中的标识符前时,会对该标识符进行参数替换并将结果用引号括起,实际上创建了一个字符串字面量。此外,预处理器会添加反斜杠来转义嵌入字符串字面量(如果存在)周围的引号,并根据需要将字符串内的反斜杠加倍。所有首尾空白符会被移除,文本中间(但不包括嵌入字符串字面量内部)的连续空白符序列会被压缩为单个空格。此操作称为“字符串化”。若字符串化的结果不是有效的字符串字面量,则行为未定义。

# 出现在 __VA_ARGS__ 之前时,整个展开的 __VA_ARGS__ 会被引号包围:

#define showlist(...) puts(#__VA_ARGS__)
showlist();            // expands to puts("")
showlist(1, "x", int); // expands to puts("1, \"x\", int")
(since 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++标准版本,展开为值
  • 199711L (C++11前) ,
  • 201103L (C++11) ,
  • 201402L (C++14) ,
  • 201703L (C++17) ,
  • 202002L (C++20) , 或
  • 202302L (C++23)
    (宏常量)
__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_EMBED_NOT_FOUND__ __STDC_EMBED_FOUND__ __STDC_EMBED_EMPTY__
(C++26)
分别展开为 0 1 2
(宏常量)

以下额外的宏名称可能由实现预定义:

__STDC__
若存在则为实现定义的值,通常用于指示C语言符合性
(宏常量)
__STDC_VERSION__
(C++11)
若存在则为实现定义的值
(宏常量)
__STDC_ISO_10646__
(C++11)

wchar_t 使用Unicode,则展开为 yyyymmL 形式的整型常量,该日期表示所支持的最新Unicode修订版本

(C++23前)

若存在则为实现定义的值

(C++23起)

(宏常量)
__STDC_MB_MIGHT_NEQ_WC__
(C++11)
若基本字符集的成员 x 可能满足 'x' == L 'x' 为假(例如在基于EBCDIC且对 wchar_t 使用Unicode的系统中),则展开为 1
(宏常量)
__STDCPP_THREADS__
(C++11)
若程序可拥有多个执行线程则展开为 1
(宏常量)
__STDCPP_STRICT_POINTER_SAFETY__
(C++11) (removed in C++23)
当实现支持严格 std::pointer_safety 时展开为 1
(宏常量)

这些宏的值(除了 __FILE__ __LINE__ )在整个翻译单元中保持不变。尝试重新定义或取消定义这些宏会导致未定义行为。

语言特性测试宏

标准定义了一组与 C++11 或之后版本引入的 C++ 语言特性相对应的预处理器宏。它们旨在提供一种简单且可移植的方法来检测所述特性的存在。

详见 特性测试

(始于 C++20)


注释

函数局部预定义变量 __func__ 并非预定义宏,但通常与 __FILE__ __LINE__ 配合使用,例如被 assert 所使用。

(C++11 起)

示例

#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++ 标准。

缺陷报告 适用标准 发布时行为 正确行为
CWG 2908 C++98 未明确说明 __LINE__ 展开为当前物理行号还是逻辑行号 展开为当前物理行号
LWG 294 C++98 包含标准库头文件的翻译单元可能包含定义其他标准库头文件中声明名称的宏 禁止此行为
P2621R2 C++23 不允许通过令牌拼接形成通用字符名称 允许此操作

另请参阅

C++ 文档 关于 宏符号索引
C 文档 关于 文本宏替换