有一次 code review,同事寫了這樣一行:
int result = MAX(sensor_read(), threshold);
我看了一眼沒說什麼,但心裡其實有點毛。
MAX 是怎麼定義的?他說是標準的那種:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
那 sensor_read() 會被呼叫幾次?他楞了一下。答案是:不一定,可能是一次,也可能是兩次。
巨集展開才是真相
C 的 #define 巨集是文字替換,preprocessor 在編譯前就把它展開,不是函式呼叫。
所以這行:
int result = MAX(a++, b++);
展開後長這樣:
int result = ((a++) > (b++) ? (a++) : (b++));
問題出在哪?被選中的那個變數,會被 ++ 兩次。
跑一個簡單的例子:
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int a = 5, b = 3;
int result = MAX(a++, b++);
printf("result = %d, a = %d, b = %d\n", result, a, b);
return 0;
}
你預期結果是什麼?result = 5, a = 6, b = 4?
實際輸出(GCC):
result = 6, a = 7, b = 4
a 被遞增了兩次。因為 a > b 成立,所以條件式選了 a++,然後又執行了一次。
為什麼這麼難發現
這種 bug 特別壞,有幾個原因。
編譯器不會報錯。 語法完全合法,-Wall -Wextra 也不會有任何警告(至少在大多數情況下)。
大部分時候看起來沒問題。 如果你傳進去的是普通變數而不是帶副作用的表達式,結果完全正確。問題只在你傳 a++、i++、func() 這種東西進去才會炸。
測試不容易覆蓋到。 你的單元測試大概不會特別針對「傳遞帶副作用的表達式給巨集」這個情境。
哪些表達式帶有副作用
這裡說的副作用(side effect),指的是表達式被求值(evaluate)時,除了產生一個值,還會改變某個狀態。
常見的:
| 表達式 | 副作用 |
|---|---|
a++ / a-- |
改變 a 的值 |
++a / --a |
改變 a 的值 |
func() |
函式執行產生的任何影響 |
a = b |
改變 a 的值 |
a += 1 |
改變 a 的值 |
傳這些東西進多重展開的巨集,就等著踩坑。
怎麼解
方法一:改用 inline 函式(現代 C 的標準做法)
static inline int max_int(int a, int b) {
return a > b ? a : b;
}
函式呼叫的規則是:每個參數只被求值一次,結果才傳進去。a++ 只會遞增一次,不會有驚喜。
缺點是型別限定了。如果你真的需要泛型,C11 有 _Generic,但用起來比較醜就是了。
方法二:GNU Extension — Statement Expression
GCC 和 Clang 支援一個擴充語法,讓巨集裡的變數只被求值一次:
#define MAX(a, b) \
({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})
用臨時變數把 a、b 先存起來,展開後就不會有多重求值的問題。
這個做法在 Linux kernel 原始碼裡到處都是。如果你的環境固定是 GCC/Clang,拿來用沒問題。但嚴格的 ISO C(-std=c99、-std=c11 配上 -pedantic)會抱怨,移植性要注意。
方法三:如果一定要用舊式巨集,文件清楚說明限制
有些 legacy 環境就是只能用純 #define,那至少把限制寫清楚:
/* WARNING: Arguments must not have side effects.
* Do not use expressions like a++, b++, or function calls. */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
不完美,但總比讓下一個人踩坑好。
那 Linux kernel 的 max() 怎麼寫
順手翻了一下,include/linux/minmax.h(kernel 5.x 以上)大致是:
#define max(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
中間那行 (void) (&_max1 == &_max2) 看起來莫名其妙,其實是個型別檢查的技巧——如果 x 和 y 型別不相容,編譯器會發出警告。是個有趣的小花招。
我的建議
現代 C(C99 以後)直接用 static inline,一勞永逸。需要多型別的場合才考慮 _Generic 或 GCC extension。
舊式的 #define MAX(a, b) 巨集,傳進去的東西一律當成「只能求值一次」來對待。不是你記不記得住,是這個規則根本不該靠人記。
小結
MAX(a++, b++) 這種寫法,不是語法錯誤,但行為不是你想的那樣。
這是 C 巨集的本質問題:它是文字替換,不是函式呼叫。每次展開,參數被求值幾次,完全取決於巨集的展開形式。帶副作用的表達式一旦被展開超過一次,就是未定義行為的前哨,或者至少是邏輯錯誤。
我自己的習慣是:能用 inline 函式的地方,就不用帶參數的巨集。除非有很強的理由(跨型別、某些極端的效能場合),不然就不值得賭。
💬 你的環境有用過這種踩坑的巨集嗎?或者你有更好的替代方案?留言聊聊。