漫談 C/C++ 巨集展開規則

最近工作上遇到一個和 C/C++ 巨集有關的問題。我想要使用巨集將原本呼叫 bcopy 的程式碼導向另一個實作 __bionic_bcopy。所以我寫出以下的程式碼:

#define bcopy __bionic_bcopy

然而這段程式碼會使得部分程式無法被正常地編譯。如果我將上面的 #define 改為:

#define bcopy(src, dst, size) __bionic_copy(src, dst, size)

原本被第一個 #define 弄壞的程式就能被正常地編譯了。然而這兩個 #define 的差異是什麼呢?以下是引起錯誤的程式片段:

builtin.def

#ifndef BUILTIN_LIBC
#define BUILTIN_LIBC(NAME) BUILTIN(NAME)
#endif

BUILTIN_LIBC(bcopy)

builtin.cpp

enum BuiltinFunction {
#define BUILTIN(NAME) BUILTIN_##NAME,
#include "builtin.def"
};

void HandleBuiltin(BuiltinFunction func) {
  if (func == BUILTIN_bcopy) {
    // ...
  }
}

這段程式碼在前置處理(Preprocessing)之後會變成:

enum BuiltinFunction {
  BUILTIN_bcopy,
};

void HandleBuiltin(BuiltinFunction func) {
  if (func == BUILTIN_bcopy) {
    // ...
  }
}

簡單的說,這段程式碼先定義了 BuiltinFunction 列舉,接著 HandleBuiltin 函式會比較參數 funcBUILTIN_bcopy

如果我們在 builtin.cpp 的最前面加上本文第一個 #define

#define bcopy __bionic_bcopy

enum BuiltinFunction {
#define BUILTIN(NAME) BUILTIN_##NAME,
#include "builtin.def"
};

void HandleBuiltin(BuiltinFunction func) {
  if (func == BUILTIN_bcopy) {
    // ...
  }
}

經過前置處理(Preprocessing)之後,程式碼會變成:

enum BuiltinFunction {
  BUILTIN___bionic_bcopy,
};

void HandleBuiltin(BuiltinFunction func) {
  if (func == BUILTIN_bcopy) {
    // ...
  }
}

因為列舉裡面的 BUILTIN_bcopy 變成 BUILTIN___bionic_bcopy,所以後面的 BUILTIN_bcopy 會被編譯器視為未宣告的識別字,進而產生編譯錯誤。

這裡有兩個巨集展開規則的細節:

  1. 當前置處理器在代換一個類函式巨集呼叫(Function-like Macro Invocation)的時候,如果參照 Formal Parameter(型式參數)的地方與 ### 運算元無關,前置處理器會先展開 Actual Parameter(實際參數),再代入展開後的 Actual Parameter。
  2. 當整個類函式巨集呼叫被展開之後,如果展開後的代換序列(Replacement List)仍然包含其他巨集參照,且被參照的巨集不是正在展開過程中的巨集[1],前置處理器會反覆代換直到沒有可以被代換的巨集參照。

以上面的 builtin.cpp 為例,前置處理器的處理步驟分別是:

  1. 因為 BUILTIN_LIBC 是一個類函式巨集,所以要將 builtin.def 裡面的 BUILTIN_LIBC(bcopy) 展開。
  2. BUILTIN_LIBC(NAME) 代換為 BUILTIN(NAME)。因為 NAME### 無關,所以前置處理器必須先展開 NAME 對應的序列。因為 bcopy 是一個類物件巨集(Object-like Macro),前置處理器會先將 bcopy 展開為 __bionic_bcopy。之後代入 __bionic_bcopy 得到 BUILTIN(__bionic_bcopy)
  3. 因為 BUILTIN 也是一個類函式巨集,所以要進一步展開。
  4. BUILTIN(NAME) 代換為 BUILTIN_##NAME,。因為 NAME 的前面有 ## 所以就直接代入 __bionic_bcopy得到 BUILTIN___bionic_bcopy,
  5. 因為 BUILTIN___bionic_bcopy 不是巨集名稱,所以前置處理器的工作到此結束。

那為什麼本文第二個 #define 不會有相同的問題呢?

#define bcopy(src, dst, size) __bionic_copy(src, dst, size)

第二個定義是類函式巨集(Function-like Macro)如果沒有傳遞參數的刮號,前置處理器就不會展開它們。在上面的例子中,前置處理器在第二步展開「NAME 對應的序列」的時候,依然會保留 bcopy

其他應用

字串化(Stringification)

有時候我們會想要將一個 #define 對應的數值串接於一個字串的字面常數(String Literal)。這時我們就會需要利用前置處理器會展開 Actual Parameter(實際參數)的特性:

#define STR(X) #X
#define XSTR(X) STR(X)

#define CONFIG 4

const char example1[] = STR(CONFIG);  // "CONFIG"
const char example2[] = XSTR(CONFIG);  // "4"

上面的例子中,因為 STR(X) 參照 X 的地方有 # 運算元,所以 X 對應的序列(即 CONFIG)會直接被字串化。如果我們要看到的是 CONFIG 的值,我們必須再將 STR 包裝為 XSTR。在 XSTR 被代換為 STR 的過程中,因為 X# 運算元無關,所以會先將 CONFIG 展開成為 4,再交由 STR4 字串化。

阻止類函式巨集展開

有時候我們編寫 Header File 的時候,可能會用到一些常見的識別字(例如:std::minstd::max),但我們希望避免這些識別字被同名的類函式巨集代換掉。此時就可以宣告一個空白的巨集打斷前置處理器的巨集代換:

#define PREVENT_MACRO_SUBSTITUTION

namespace test {

template <typename T>
T max PREVENT_MACRO_SUBSTITUTION (T a, T b) {
  using std::max;
  return max PREVENT_MACRO_SUBSTITUTION (a, b);
}

}  // namespace test

即使我在上面的程式碼前面插入下面的 #define 也不會影響產生的程式碼:

#define max(a, b) (((a) > (b)) ? (a) : (b))

因為當前置處理器看到 max 的時候,它知道 max 是一個類函式巨集,但是它沒有看到傳遞參數的刮號,所以會保留原樣。

備註:這個技巧我是從 Boost Library 的文件看到的。如果你可以使用 Boost Library,你可以直接使用 boost/config/suffix.hpp 裡面的 BOOST_PREVENT_MACRO_SUBSTITUTION

結語

本文從一個巨集展開的問題談起,先介紹了巨集展開規則,然後解釋了類物件巨集(Object-like Macro)與類函式巨集(Function-like Macro)的差異。最後,本文以兩種常見的前置處理技巧作為結尾。希望大家看完本文之後,能對 C/C++ 前置處理器有進一步的認識。

參考資料

致謝

感謝讀者 Stimim 指出巨集展開的過程中,部分巨集參照不會再次被代換。目前顯示的文章是更正後的版本。


[1]

具體的例子是遞迴的巨集不會被再次展開:

#define EXAMPLE(X) (EXAMPLE(X + 1) - EXAMPLE(1))
EXAMPLE(5)

如下所示,展開後 EXAMPLE 會被保留下來:

(EXAMPLE(5 + 1) - EXAMPLE(1))

C++ 標準是以下面的句子描述巨集反覆展開的規則:

If the name of the macro being replaced is found during this scan of the replacement list (not including the rest of the source file's preprocessing tokens), it is not replaced. Furthermore, if any nested replacements encounter the name of the macro being replaced, it is not replaced. These nonreplaced macro name preprocessing tokens are no longer available for further replacement even if they are later (re)examined in contexts in which that macro name preprocessing token would otherwise have been replaced.

這個句子的大意是:如果巨集參照的對象是正在展開過程中的巨集,則這些巨集參照就不會被展開。除此之外,這些巨集參照再也不會被當成「可以展開」的巨集參照。

然而 C++ 語言瑕疵報告 CWG#268 指出「the name of the macro being replace」有歧義。CWG#268 提出以下範例:

#define IDENTITY(X) X
#define TEST_0(X) IDENTITY(X)
#define TEST_1(X) IDENTITY(TEST_0)(X)
TEST_1(42)

根據不同的解釋,TEST_1(42) 能被展開為 IDENTITY(42) 或是 42。早期標準訂立的過程中,預期的結果是 42。筆者測試 GCC 與 Clang 的展開結果也是 42。然而另一個解讀並不違反現有標準。根據 CWG#268 的描述,標準委員會知道有這樣的差異,但是因為沒有迫切需求就沒有更動標準裡的文字。

Note

如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。