Skip to content
📖 Auto 版 Modern C本书目录

第十七章:函数式宏 ​

级别 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 类型、作用域规则或语法一无所知。它执行字面文本替换。

宏展开分几步进行:

  1. 扫描标记序列查找宏调用。
  2. 替换参数(处理 # 和 ##)。
  3. 重新扫描结果查找更多宏。
  4. 重复直到没有更多替换可能。

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
}

基于 MIT 许可发布。