%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** ❓如何利用宏实现**条件编译**? ❓如何利用宏来判断实现**条件编译**? # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # C/C++中的宏 > **宏**(也称 "**==预处理宏==**" preprocessor macro) **宏**是**预处理器的一种特性**,其在 "**预处理阶段"(编译之前)==对代码进行文本替换==**。 宏的定义/取消定义:通过**预处理指令**完成 - **定义宏**: `#define macro` - **取消宏定义**:`#undef macro` ### 宏的类型 宏主要有两种类型: - **对象宏**(Object-like Macros):没有参数,仅做**简单的文本替换**。 - **函数宏**(Function-like Macros):可接受参数,并在**使用时进行替换**。 ```c // 对象宏 #define PI 3.14159 #define MAX_BUFFER_SIZE 1024 // 函数宏 #define SQUARE(x) ((x)*(x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) int main() { double radius = 2.0; double area = PI * SQUARE(radius); bool res = MAX(area, 5); char buffer[MAX_BUFFER_SIZE]; } ``` ### 宏的作用 作用: - **==条件编译==**:将宏与预处理指令结合使用,实现条件编译,**控制哪些代码片段被编译**。 - **==参数化代码==**:函数宏允许创建**参数化代码片段**,避免重复代码。 缺点: - **不进行类型检查**:宏是简单的文本替换,不进行类型检查,这可能导致难以调试的错误。 - **作用域问题**:宏在整个代码中全局有效,没有作用域的限制,可能导致命名冲突。 - **代码膨胀**:宏展开时会导致代码膨胀,特别是在使用函数宏时,每次调用都会替换为完整的表达式。 - **调试困难**:宏替换发生在编译之前,**调试时不能直接看到宏展开后的代码**,增加了调试的难度。 <br> ### C++标准提供的预定义宏 C++ 标准规定了一系列**预定义的宏常量**,可供在代码中使用,有助于调试: - `__FILE__` 展开为字符串字面量,值为当前代码所在**源文件的文件名**。例如 "main.cpp"。 - `__LINE__` 展开为整型字面量,值为 该宏在**源代码中的行号**。 - `__DATE__` 展开为字符串字面量,值为 **编译当前文件的日期**,格式为 `"MMM DD YYYY"`。 - `__TIME__` 展开为字符串字面量,值为 **编译当前文件的本地时间**,格式为 `"HH:MM:SS"`。 - `__func__` 展开为**当前代码所在的函数名**(since C++11) 使用示例: ```cpp #define ERROR_MSG(msg) \ fprintf(stderr, "ERROR[ %s:%d, %s: %s\n], __FILE__, __LINE__, __func__, msg"); void func() { ERROR_MSG("Invalid input"); // 输出: [ERROR] main.cpp:10, func: Invalid input; } ``` <br><br><br> # 条件编译 C/C++提供以下**预处理指令**,可供实现**条件编译**: 1. `#ifdef`, `#ifndef`: 以 "**==宏是否被定义==**" 作为判断条件 2. `#if`, `#elif` , `#else` , `#endif`: 以 "**==基本表达式==**" 作为判断条件 ### 示例 示例一:利用条件编译实现 "仅引入一次头文件" ```cpp // 利用条件编译 #ifndef __MYPROG_H__ #define __MYPROG_H__ ... #endif // 上述等价于预处理指令#progma once ``` 示例二:利用条件编译控制 "代码片段" 编译 ```cpp int main() { #ifdef FEATURE_ENABLED cout << "Feature is enabled." << endl; #else cout << "Feature is disabled." << endl; #endif return 0; } // 编译命令: g++ -DFEATURE_ENABLED -o prog main.cpp ``` 示例三:利用条件编译控制 "代码片段" 编译 ```cpp int main() { #if VERSION == 1 cout << "Version 1" << endl; #elif VERSION == 2 cout << "Version 2" << endl; #else cout << "Unknown version" << endl; #endif return 0; } // 编译命令: g++ -DVERSION=2 -o prog main.cpp ``` 示例四: ```cpp // 假设这是一个跨平台项目 #if defined(_WIN32) || defined(_WIN64) #define PLATFORM "Windows" #elif defined(__linux__) #define PLATFORM "Linux" #elif defined(__APPLE__) && defined(__MACH__) #define PLATFORM "MacOS" #else #define PLATFORM "Unknown" #endif int main() { cout << "Platform: " << PLATFORM << endl; } ``` <br><br> # 宏参数转字符串 # 宏参数操作符 两个用于宏定义中的操作符,在预处理阶段被处理: - `#` :宏参数的 "**字符串化**" 操作符, 用于**将宏的参数转换为==字符串字面量==**; - `##`:宏参数的 "**连接**" 操作符, 用于**将两个标记(token)拼接得到一个新标记**,要求至少其中一个 token 是宏参数。 > [!example] `#` 使用示例 > > 如下示例,`#x` 表示将**宏参数 `x`** 转换为**字符串常量**。 > > ```c > #defind str_temp(x) #x > #define str(x) str_temp(x) > ``` > > 使用两个宏的原因:**确保==宏参数==在被字符串化之前先进行展开**。 > > - `str_tempx(x) #x` 这里,**当参数 `x` 是一个==宏定义的标识符==时**(即存在 `#define x ...`),`#x` 不会展开 `x` 的值,而是**得到字符串 `x` 本身** > - 通过为 `str(x)` 引入中间宏 `str_temp(x)`,使得**参数 `x` 在传递给 `str_temp` 时会被展开得到值**,从而将 `x` 对应的值字符串化。 > > ```cpp > #define VALUE 123 > #define str_temp(x) #x > #define str(x) str_temp(x) > > int main () { > cout << str(VALUE) <<endl; // 输出: "123". `str(VALUE)`展开为`str_temp(123)`展开为`#123`, 得到字符串"123". > cout << str_temp(VALUE) <<endl; // 输出: "VALUE". `str_temp(VALUE)` 展开为 #VALUE , 因此得到字符串"VALUE". > return 0; > } > ``` > > [!example] `##` 使用示例 > > ```cpp > #define concat_temp(x, y) x##y > > int main() { > int xy = 42; > std:: cout << concat_temp(x, y) << std:: endl; // 输出: 42 > return 0; > } > ``` > > 连接函数名: > > ```cpp > #define FUNCTION_NAME(func, num) func##num > > void foo1() { > std:: cout << "Function foo1 called" << std:: endl; > } > > void foo2() { > std:: cout << "Function foo2 called" << std:: endl; > } > > int main() { > FUNCTION_NAME(foo, 1)(); // 调用 foo1 函数 > FUNCTION_NAME(foo, 2)(); // 调用 foo2 函数 > return 0; > } > ``` > > 连接变量名: > > ```cpp > #define UNIQUE_NAME(base, id) base##id > > int main() { > int UNIQUE_NAME(var, 1) = 10; > int UNIQUE_NAME(var, 2) = 20; > > std:: cout << var1 << std:: endl; // 输出: 10 > std:: cout << var2 << std:: endl; // 输出: 20 > > return 0; > } > ``` > > # 宏可变参数 > Variadic Macros(C99 引入,C++11 延用) 支持在宏定义中引入**可变数量的宏参数**,常用于日志打印等场景。 基本语法: 1. 宏定义中,通过 `...` 标识可变参数,可以是 0 或多个参数。 2. 宏替换文本中,通过 **`__VA_ARGS__`** 标识符引用可变参数。 > [!example] 使用示例:日志宏 > > ```cpp > #define LOG_INTERNAL__(logger, level, clevel, size, fmt, ...) \ > do { \ > if (logger != nullptr && logger->isEnabledFor(level)) { \ > char msg_buf[size]; \ > log::format(msg_buf, size, fmt, ##__VA_ARGS__); \ > log::async_forced_log(logger, level, msg_buf, LOG4CXX_LOCATION); \ > if (log::isStreamLogEnable) { \ > LOGCXX:Log(clevel, __CSDKFILE__, __LINE__, "", msg_buf); \ > } \ > } \ > } while (false); > > #define LOGF_INFO(fmt, ...) \ > LOG_INTERNAL__(log::root_logger, Level::getInfo(), \ > LOGCXX::LEVEL::CINFO, log::MAX_SIZE_INFO, fmt, \ > ##__VA__ARGS__); > > > #define INFO(fmt, ...) LOGF_INFO(fmt, ##__VA_ARS__) > > > INFO("%s", "hello, world"); // 使用上述宏定义的日志打印 > ``` > > > [!caution] GCC/Clang 扩展支持 `##__VA_ARGS` ,用于**在可变参数为空时去掉前面多余的逗号** > > GCC/Clang 支持使用 `##_VA_ARGS__` 处理可变宏参数为空时去除前面多余逗号。 > 注意,在 MSVC 中,可变参数为空时的逗号处理不同,不支持这个扩展。 > > ```cpp > // 不使用 `##` 的错误示例 > #define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__) > LOG("Hello"); // 展开为 `printf("[LOG] Hello\n", )` > // 可变参数数量为0, 多余逗号导致编译错误 > > // 使用 `##_VA_ARGS__` 的正确示例 > > #define LOG(fmt, ...) printf("[LOG] " fmt "\n", ##__VA_ARGS__) > > LOG("Hello"); // 展开为 printf("[LOG] Hello\n") > // ##会在__VA_ARGS__为空时, 移除其前面的逗号 > ``` > > <br><br><br> # 宏定义注意事项🚨 > 参见 [^1] 1. **避免使用宏 `#define`** 来定义**常量**,而使用 `const`、`enum`、或 `inline` - **==宏定义==会被预处理器全部替换,编译器见不到"宏"名称,只可见替换后的内容**,不利于 debug。 2. 需要 **确保宏语句的独立性** 1. 对**整个表达式&各操作数**加外层括号; 2. 用 `do {...} while(0)` 包裹) 3. **避免使用宏的同时修改操作数** - 例如对于宏 `#define MAX(a, b) ((a) > (b) ? (a) : (b))`,使用时 `MAX(++a, b)` ,可能导致 `++a` 执行两次。 <br> ## (1)防止宏展开后导致错误代码逻辑 **宏定义中涉及==操作符==、==多条语句==时,在宏被替换展开后可能导致意想不到的错误代码逻辑**。例如: 1. 可能**受运算符优先级**等影响,导致展开后得到**不符合预期的错误运算顺序**。 2. 可能受 `if-else` 语句最近结合的影响,导致错误的语句块匹配。 ##### (1.1)宏定义中具有 "运算符",为表达式时 > [!NOTE] 要求 > > 1. **表达式==最外层==需要加==括号==**; > 2. **表达式中每一个 "==操作数==" 均需加==括号==**; > 示例一: `#define SQUARE(x) ((x)*(x))` ```cpp // 错误定义 #define SQUARE(x) x * x int res = SQUARE(2 + 3); // 操作数未加括号, 展开为 2 + 3 * 2 + 3, 导致错误运算顺序 // 正确定义 #define SQUARE(x) ((x) * (x) int res = SQUARE(2 + 3); // 展开为((2+3)*(2+3)). ``` 示例二:`#define ADD(x, y) ((x)+(y))` ```cpp // 错误定义 #define ADD(x, y) x + y int res = ADD(x, y) * z; // 表达式无外层括号, 展开为x + y * z, 与运算符*先结合了, 导致错误运算顺序, // 正确定义 #define ADD(x, y) ((x)+(y)) int res = ADD(x, y) * z // 展开为((x)+(y)) * z, 运算顺序不受影响. ``` 示例三:`#define MAX(a, b) ((a) > (b) ? (a) : (b))` ```cpp // 错误定义 #define MAX(a, b) a > b ? a : b int res = MAX(a, b&c); // 操作数未加括号, 展开为a > b&c ? a : b&c, // 而比较运算>优先级高于按位与&, 故得到错误计算顺序(a > b)&c // 错误展开示例: int res = MAX(2, 0^1) // 展开为2>0^1 ? 2 : 0^1, 先运算2>0得到1, 再以1^1==0为条件判断, 故错误得到0^1而不是2. // 正确定义 #define MAX(a, b) ((a) > (b) ? (a) : (b)) ``` ##### (1.2)当宏定义中包含多条语句时 > [!NOTE] 要求—将所有语句**用 `do {...} while(0)`包裹** 或 **用 `({...})` 包裹**,或者,保证展开时得到单独语句块。 > > 注:`({...})` 是 GCC 提供的语法扩展,用于将多个语句封装成一个表达式,非 C++标准语法 错误示例: ```cpp // 错误定义 #define SOME_MACRO(cond) \ if (cond) printf("true") // 使用时: if (cond1) SOME_MACRO(cond2); else ... // 展开为 if (cond1) if (cond2) printf("true"); else // 此处的else将匹配最近的if, 即上一行. 导致原本预期cond1不成立时应执行的op, 被省略了. ... ``` 正确示例: ```cpp #define PRINT_MAX(a, b) \ do { \ if ((a) > (b)) { \ printf("%d", (a)); \ } else { \ printf("%d", (b)); \ } \ } while (0); ``` > [!quote] > ![[_attachment/02-开发笔记/01-cpp/预处理相关/cpp-宏.assets/IMG-cpp-宏-A87A2C5800609552F35E0D10144639D5.png|687]] > [!example] 示例:linux ext4 文件系统中 `ext4.h` 里的使用[^2] > ![[_attachment/02-开发笔记/01-cpp/预处理相关/cpp-宏.assets/IMG-cpp-宏-56CB5478C3A3C00C6F2B8C0F1965A489.png|557]] <br> ## (2)避免使用宏的同时 "修改操作数" 错误示例如下,应**避免对传给宏的参数进行操作**。 ```cpp #define MAX(a, b) ((a) > (b) ? (a) : (b)) // 使用宏的同时对参数进行自增, 展开得到`((++a) > (b) ? (++a) : (b))`, 可能导致a被自增两次, 重复计算 int res = MAX(++a, b); ``` ## 使用建议 <br><br><br> # 宏定义使用示例 ## 基于布尔宏的条件判断选择 说明:实现一个 "**==函数宏==**" ,可用于对 "**另一个==布尔宏==**" 进行**条件判断**,**根据==该宏的值是否存在==来选择==不同的替换结果==**。 示例: ```C struct {   word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];   vaddr_t pc; } MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state); ``` 实现过程: ![[_attachment/02-开发笔记/01-cpp/预处理相关/cpp-宏.assets/IMG-cpp-宏-DF06A8A81CCCA08F70BEBAED0AE346CF.png|733]] 实现说明: 明确 `macro` 是个**布尔宏**,其值只能是 0 或者 1。 - 假设**宏 `macro` 存在**,则其值**规定只能是 0 或者 1**,则有如下展开: - `MUXDEF(macro, X, Y)` - => `MUX_MACRO_PROPERTY(__P_DEF_, 1, X, Y)`,**==注意这里宏 `macro` 的值被替换==** - => `MUX_WITH_COMMA(concat(__P_DEF_, 1), X, Y)` ,即 `MUX_WITH_COMMA(__P_DEF_1, X, Y)` - => `MUX_WITH_COMMA(X,, X, Y)` ,**==注意这里 `_P_DEF_1` 或 `_P_DEF_0` 均会展开为 "`X,`"==** - => `CHOOSE2nd(X, X, Y)` - => `X`,宏最终被替换为 X。 - 假设**宏 `macro` 不存在**,则有如下展开: - `MUXDEF(macro, X, Y)` - => `MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)`,**==注意这里宏 `macro` 由于未定义,因此保留着 `macro` 字符串本身==** - => `MUX_WITH_COMMA(concat(__P_DEF_, macro), X, Y)` ,即 `MUX_WITH_COMMA(__P_DEF_macro, X, Y)` - => `CHOOSE2nd(__P_DEF_macro X, Y)` - => `Y`,宏最终被替换为 Y。 核心思想是,根据 "**宏 `macro`" 是否存在以及==其值的 0/1==**,对该**宏字符串**进行拼接:`_P_macro` , 同时预先**定义两个宏 `_P_0` 和 `_P_1`** **以进行替换**,如果 `_P_macro` 能被解释为 `_P_0` 或 `_P_1`,则替换为 `X,` **多一项逗号**,从而**将原本的参数`(_P_macro X, Y)` 替换为 `X, X, Y`**,由于是**固定选择 "第二项"**,因此就实现了 "**条件选择**"。 > [!caution] 真的是奇技淫巧!实际开发大概很少需要这样吧? 分析起来有点复杂,不是很直观 <br><br><br> # 断言 > [!NOTE] `assert` 与 `static_assert` 的区别 > > 用途与检查时机不同: > > - `assert` 用于 **==在运行时== 检查条件**,通常用于**调试阶段**检查逻辑错误; > - `static_assert` 用于 **==在编译时== 检查条件**,用于**检查类型大小**、**模板参数**等编译时可知的属性或约束。 > > 错误处理不同: > > - `assert` 失败会导致**程序终止执行**,并可以**打印一条错误消息**; > - `static_assert` 失败会导致**编译错误**,阻止程序编译通过。 > > 可否禁用上: > > - `assert` 可以通过定义**==宏 `NDEBUG`==** 来禁用, > - `static_assert` 不能被禁用,因为它是**编译时的一部分**。 <br> ## `assert` 断言 `assert` 断言是定义在**头文件 `<cassert>`** 中的**宏**,用于 **==在运行时==检查程序中的某个条件是否为真**。 如果**条件为假,assert 会导致程序终止**,并**打印一条错误消息到标准错误输出,指出失败的条件以及发生失败的源文件和行号**。 > [!important] `assert` 依赖于 **名为 `NDBUG` 的预处理宏** [^3](P215) > > - 默认未定义 `NDEBUG` ,此时 `assert` 将执行运行时检查。 > - **当定义了宏 `NDEBUG`** 时,**所有 `assert` 调用会被禁用**,因此**不会对 release 版本的性能造成影响**。 > > 因此,Release 版本发布时,通常都会指定编译选项 `-DBDEBUG`,引入`NDEBUG` 宏,使 `assert()` 失效。 <br> ### `NDEBUG` 宏 **定义 `NDBUG` 宏**: - 在源文件中定义: `#define NDEBUG`; - 编译时指定:`gcc -D NDEBUG main.c`; - CMake 中 `CMAKE_CXX_FLAGS_RELEASE` 变量默认即为 `-03 -DNDEBUG`。 `NDEBUG` 宏除了影响 `assert` 宏之外,也可以基于 `NDEBUG` 宏**自定义条件调试代码**: ```cpp void print(const int ia[], size_t size) { #ifndef NDEBUG // __func__ 是编译器定义的一个局部静态变量, 用于存放函数的名字 cerr << __func__ << ": array size is " << size <<endl; #endif // ... } ``` <br> ## `static_assert` 静态断言 > `static_assert` 是 C++关键字,直接使用,并不属于 `<cassert>` 头文件。 `static_assert` 用于 **==在编译时==进行断言检查**,**检查==常量表达式==的值**。 ```cpp // 如果`int`类型不是4字节,则`static_assert`将会导致编译错误,错误信息为:"int must be 4 bytes"。 static_assert(sizeof(int) == 4, "int must be 4 bytes"); ``` <br><br> # 参考资料 [Test if preprocessor symbol is defined inside macro](https://stackoverflow.com/questions/26099745/test-if-preprocessor-symbol-is-defined-inside-macro) # Footnotes [^1]: 《Effective C++》Item3 [^2]: [linux/fs/ext4/ext4.h at master · torvalds/linux · GitHub](https://github.com/torvalds/linux/blob/master/fs/ext4/ext4.h#L776) [^3]: 《C++ Primer》