C 和 C++ 预处理器

最后更新: 2023-06-09 08:19:02 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

预处理器在 C 或 C++ 解析器看到声明、类型、命名空间或重载之前转换预处理标记。它会展开包含文件、求值条件指令并执行宏替换。应将其输出视为生成的源代码,因为语言层面的作用域和类型检查无法修复不安全的展开结果。

包含文件搜索是一项构建工具约定 #

#include "project/header.hpp" 和 #include <library/header> 标识的是头文件名标记,而不是由语言标准规定的文件系统路径。从头文件名到源文件的映射(包括引号与尖括号两种形式的搜索顺序)由实现定义。工具链通常会针对引号形式搜索当前包含文件所在的目录和引号专用路径,而尖括号形式的头文件则从配置的系统路径开始搜索;实际顺序应查阅编译器文档和构建命令。

对于 GCC 和 Clang,可使用以下命令检查当前生效的包含文件搜索列表:

bash
printf '' | c++ -E -x c++ -v -

按照项目约定,对项目头文件使用引号形式,对已安装或标准库头文件使用尖括号形式。应在适当的构建目标中配置第三方依赖,而不是添加宽泛的全局 -I 路径。-isystem 的搜索位置不同,并且可能改变诊断处理方式,因此不能与 -I 互换。

每个项目头文件都需要一种防止多次包含的策略。传统的包含保护符可移植到所有符合标准的实现:

cpp
#ifndef PROJECT_WIDGET_HPP
#define PROJECT_WIDGET_HPP

class Widget;

#endif

#pragma once 得到了广泛支持,但不属于 ISO C 或 C++ 标准。仅当受支持的工具链接受这项依赖时才应使用。

宏替换包含多个阶段 #

对于类函数宏调用,宏参数会在替换前展开,但参数与 # 或 ## 一起使用时除外。随后还会重新扫描替换列表,以执行进一步展开。正在展开的宏暂时不能递归展开,从而防止直接的无限递归。

c
#define STRINGIZE_IMPL(x) #x
#define STRINGIZE(x) STRINGIZE_IMPL(x)
#define VERSION 42

const char* version = STRINGIZE(VERSION); // "42"

第一层将展开后的 VERSION 传递给实现宏;直接使用 #VERSION 会产生 "VERSION"。字符串化会保留标记的拼写,但受预处理器的空白和转义规则约束。使用 ## 拼接标记时,结果必须构成一个有效的预处理标记。

c
#define JOIN_IMPL(a, b) a ## b
#define JOIN(a, b) JOIN_IMPL(a, b)

int JOIN(worker_, VERSION); // worker_42

应谨慎使用生成的标识符。拼接后的名称会让普通代码搜索无法发现声明,并可能在远离宏调用的位置产生诊断信息。

条件编译与特性检测 #

条件指令在宏替换后对整型常量表达式进行操作。应显式检测配置宏,并将平台选择逻辑放在需要它的边界附近。

cpp
#if defined(_WIN32)
#  include <windows.h>
#elif defined(__unix__)
#  include <unistd.h>
#else
#  error "Unsupported platform"
#endif

对于语言特性,应在所选语言模式公开相应宏时使用 __cpp_concepts 等标准化特性检测宏。对于库特性,应包含相关头文件、检测对应的 __cpp_lib_* 宏,并在可用时从 <version> 获取该宏。在支持 __has_include 运算符的实现中,可以用它选择可选头文件,但找到头文件并不能证明其 API 或 ABI 与所需版本匹配。

对不可能成立的配置使用 #error。对于能够在编译前检测的组合,应优先由构建系统给出诊断,因为目标平台、编译器和依赖版本均由构建系统管理。

安全的宏边界 #

只要语言能够表达相应操作,就应优先使用 constexpr 变量、内联函数、模板、枚举和具有作用域的类型。它们能够保留类型检查、作用域、重载决议和调试器可见性。

仍有必要保留的宏必须为每个表达式参数和完整的表达式结果添加括号:

c
#define SQUARE(x) ((x) * (x))

括号无法保证重复求值的安全性:SQUARE(index++) 会递增两次。宏必须说明每个参数是否只求值一次;除非有意需要相应行为,否则调用点不应传入带有副作用的表达式。

对于展开为语句的宏,应使用单语句模式:

c
#define LOG_FAILURE(message) \
    do {                    \
        log(message);       \
    } while (0)

该模式使 if 语句能够将宏调用视为一条语句。它并不提供 C++ 作用域、类型、异常安全或同步机制。宏应限制在可移植性、条件编译或标记生成边界,因为只有这些边界需要并能约束宏的文本性质。

评论

在 GitHub 上阅读或评论(发表评论需要登录)