[C 的那些眉角]巨集函式的副作用 — MAX(a++, b++) 會怎樣

有一次 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;  \
    })

用臨時變數把 ab 先存起來,展開後就不會有多重求值的問題。

這個做法在 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) 看起來莫名其妙,其實是個型別檢查的技巧——如果 xy 型別不相容,編譯器會發出警告。是個有趣的小花招。


我的建議

現代 C(C99 以後)直接用 static inline,一勞永逸。需要多型別的場合才考慮 _Generic 或 GCC extension。

舊式的 #define MAX(a, b) 巨集,傳進去的東西一律當成「只能求值一次」來對待。不是你記不記得住,是這個規則根本不該靠人記。


小結

MAX(a++, b++) 這種寫法,不是語法錯誤,但行為不是你想的那樣。

這是 C 巨集的本質問題:它是文字替換,不是函式呼叫。每次展開,參數被求值幾次,完全取決於巨集的展開形式。帶副作用的表達式一旦被展開超過一次,就是未定義行為的前哨,或者至少是邏輯錯誤。

我自己的習慣是:能用 inline 函式的地方,就不用帶參數的巨集。除非有很強的理由(跨型別、某些極端的效能場合),不然就不值得賭。


💬 你的環境有用過這種踩坑的巨集嗎?或者你有更好的替代方案?留言聊聊。


發佈留言