第十七章:函数式宏
级别 3 — 经验
文本替换、预处理器技巧,以及为什么 Auto 用编译时元编程取代宏。
函数式宏是 C 最强大也最危险的特性之一。它们在编译器看到代码之前通过文本替换实现元编程。Auto 采取了根本不同的方法:它通过 #[] comptime 系统提供编译时代码生成,这是类型安全且可预测的。
17.1 宏如何工作
宏由 C 预处理器在编译前处理。预处理器执行文本替换:
c
// C 深入:宏基础
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define PI 3.14159265
int result = MAX(3, 7); // 展开为:((3) > (7) ? (3) : (7))
int area = SQUARE(5); // 展开为:((5) * (5))
double circ = 2 * PI * r; // 展开为:2 * 3.14159265 * r预处理器是翻译的独立阶段。它对 C 类型、作用域规则或语法一无所知。它执行字面文本替换。
宏展开分几步进行:
- 扫描标记序列查找宏调用。
- 替换参数(处理
#和##)。 - 重新扫描结果查找更多宏。
- 重复直到没有更多替换可能。
C 深入: 重新扫描步骤可能导致无限递归。C 标准通过不在宏自身展开期间重新展开它来防止这种情况。但宏之间的相互递归是允许的,有时在高级宏编程中被有意使用。
Auto 的方法。 Auto 没有预处理器和宏。C 中使用宏的每个操作在 Auto 中使用常规函数或 comptime 元编程:
auto
// Auto:常规函数取代宏
fn safe_max(a int, b int) int {
if a > b { a } else { b }
}
fn safe_square(n int) int {
n * n
}17.2 参数检查
宏不检查参数类型。替换是纯文本的,这会导致微妙的缺陷:
c
// C 深入:宏陷阱
#define SQUARE(x) ((x) * (x))
int a = SQUARE(2 + 3);
// 展开为:((2 + 3) * (2 + 3)) = 25(有括号时正确)
// 但是这样呢?
#define BAD_SQUARE(x) x * x
int b = BAD_SQUARE(2 + 3);
// 展开为:2 + 3 * 2 + 3 = 2 + 6 + 3 = 11(错误!)最危险的陷阱是双重求值:
c
// C 深入:双重求值
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = MAX(x++, 3);
// 展开为:((x++) > (3) ? (x++) : (3))
// 如果 x > 3,x 被递增两次!x 变成 7,不是 6。常见宏问题:
| 问题 | 示例 | 后果 |
|---|---|---|
| 缺少括号 | x * x 配 2 + 3 | 运算符优先级错误 |
| 双重求值 | MAX(x++, y) | 副作用执行两次 |
| 无类型检查 | MAX("hello", 3) | 编译通过,运行时崩溃 |
| 无法调试 | 单步进入宏 | 调试器中不可见 |
| 作用域问题 | 宏使用局部变量 | 名称冲突 |
C 深入: 安全宏的规则:始终给每个参数和整个表达式加括号。
#define SQUARE(x) ((x) * (x))。即使如此,双重求值也无法防止。唯一真正安全的宏是参数为简单标识符或常量的宏。
Auto 的方法。 Auto 函数没有这些问题:
auto
// Auto:没有宏陷阱
fn safe_max(a int, b int) int {
if a > b { a } else { b }
}
fn main() {
// 参数只求值一次
var x int = 5
let y int = safe_max(x, 3) // x 求值一次
print("x:", x) // x 仍然是 5
print("y:", y) // y 是 5
}