%%
# 纲要
> 主干纲要、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》