最近研究了一下 MAX 宏的进化史:从青铜到王者共 6 个版本(define0.c ~ define5.c),完整记录了一个三目运算符宏是怎么被优先级、副作用、类型问题一步步逼着进化的。

先上本篇的理论基础——C 语言运算符优先级表(16 级,本篇 MAX 宏的悲剧就发生在第 7 级 > 和第 8 级 != 之间):

优先级 运算符 结合律
1 后缀运算符:[] () . -> ++ -- (类型名称){列表} 从左到右
2 一元运算符:++ -- ! ~ + - * & sizeof _Alignof 从右到左
3 类型转换运算符:(类型名称) 从右到左
4 乘除法运算符:* / % 从左到右
5 加减法运算符:+ - 从左到右
6 移位运算符:<< >> 从左到右
7 关系运算符:< <= > >= 从左到右
8 相等运算符:== != 从左到右
9 位运算符 AND:& 从左到右
10 位运算符 XOR:^ 从左到右
11 位运算符 OR:| 从左到右
12 逻辑运算符 AND:&& 从左到右
13 逻辑运算符 OR:|| 从左到右
14 条件运算符:?: 从右到左
15 赋值运算符:= += -= *= /= %= &= ^= |= <<= >>= 从右到左
16 逗号运算符:, 从左到右

青铜:define0.c——裸奔的参数

1
#define MAX(x,y) x > y ? x : y

看起来没问题?调用 MAX(1 != 1, 1 != 2),先做纯文本替换:

1
int result = 1 != 1 > 1 != 2 ? 1 != 1 : 1 != 2;

然后按优先级推理:> 高于 !=,所以先算 1 > 1 得 0:

1
2
3
1 != 0 != 2 ? 0 : 1
1 != 2 ? 0 : 1
→ result = 0

明明想比较两个表达式的大小,结果完全是另一回事。

白银:define1.c——参数加括号

1
#define MAX(x,y) (x) > (y) ? (x) : (y)

再调用 3 + MAX(1, 2),展开:

1
2
3
4
int result = 3 + (1) > (2) ? (1) : (2);
// "+" 优先级高于 ">",先算 3 + 1:
int result = 4 > 2 ? 1 : 2;
// result = 1,但你想算的是 3 + MAX(1,2) = 5!

参数括号解决了参数内部的优先级问题,但整个宏表达式没有括号,和外层运算结合时照样出错。

黄金:define2.c——整体括号与新坑:副作用

1
#define MAX(x,y) ((x) > (y) ? (x) : (y))

整体加括号后,优先级问题彻底解决。但新的坑出现了——带副作用的参数

1
2
3
int i = 1, j = 0;
int result = 3 + MAX(i++, j++);
// 展开:3 + ((i++) > (j++) ? (i++) : (j++));

i++ 在展开后被执行了两次,算出来的结果总是偏大。参数带副作用时,宏不管怎么加括号都救不了——这是宏的天生缺陷。

铂金:define3.c——GNU 语句表达式

1
2
3
4
5
#define MAX(x,y) ({     \
int _x = x; \
int _y = y; \
_x > _y ? _x : _y; \
})

这是 GCC 的 语句表达式(statement expression) 扩展:({ ... }) 里可以写多条语句,最后一行的值就是表达式的值。先把参数存进中间变量 _x_y,副作用参数只求值一次,问题解决。

代价有两个:类型写死成了 int(传 float 直接出错,MAX(3.65, 3.68) 按整数比较);而且这是 GNU 扩展,牺牲了可移植性。

钻石:define4.c——把类型也做成参数

1
2
3
4
5
6
7
8
#define MAX(type, x, y) ({    \
type _x = x; \
type _y = y; \
_x > _y ? _x : _y; \
})

float result1 = MAX(float, 3.65, 3.68);
int result2 = MAX(int, 24, 15);

把类型提为宏的第一个参数,调用方自己声明比较的类型——泛型的雏形出现了。当年写到这里的时候还开玩笑说”某天公司来了个 Python 程序员一起 review 代码逻辑”。

王者:define5.c——typeof 自动推导

1
2
3
4
5
6
7
#define MAX(x, y) \
({ \
typeof(x) _x = (x); \
typeof(x) _y = (y); \
(void)(&_x == &_y); \
_x > _y ? _x : _y; \
})

终极形态:typeof(x) 自动获取传入值的类型,连类型参数都不用传了。倒数第二行的 (void)(&_x == &_y) 是一个经典技巧——当 _x_y 类型不同时,比较两个不同类型指针的地址会触发编译警告,相当于内建了一道类型检查。

注:原始笔记里这一段有个笔误(两处都写成了 (y)),整理时已修正为 (x) / (y)

复盘:六级进化各解决了什么

版本 写法 解决的问题 遗留问题
青铜 x > y ? x : y —— 优先级灾难
白银 (x) > (y) ? (x) : (y) 参数优先级 整体优先级
黄金 ((x) > (y) ? (x) : (y)) 整体优先级 参数副作用
铂金 语句表达式 + 中间变量 参数副作用 类型写死
钻石 MAX(type, x, y) 类型可指定 调用变啰嗦
王者 typeof(x) 类型自动推导 ——

宏和函数的本质区别就藏在这次进化里:宏是文本替换,函数是值传递。理解了这条,预处理器的一切怪行为都有了解释。