最近工作上遇到一個和 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
函式會比較參數 func
與 BUILTIN_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
會被編譯器視為未宣告的識別字,進而產生編譯錯誤。
這裡有兩個巨集展開規則的細節:
- 當前置處理器在代換一個類函式巨集呼叫(Function-like Macro Invocation)的時候,如果參照 Formal Parameter(型式參數)的地方與
#
或##
運算元無關,前置處理器會先展開 Actual Parameter(實際參數),再代入展開後的 Actual Parameter。 - 當整個類函式巨集呼叫被展開之後,如果展開後的代換序列(Replacement List)仍然包含其他巨集參照,且被參照的巨集不是正在展開過程中的巨集[1],前置處理器會反覆代換直到沒有可以被代換的巨集參照。
以上面的 builtin.cpp
為例,前置處理器的處理步驟分別是:
- 因為
BUILTIN_LIBC
是一個類函式巨集,所以要將builtin.def
裡面的BUILTIN_LIBC(bcopy)
展開。 - 將
BUILTIN_LIBC(NAME)
代換為BUILTIN(NAME)
。因為NAME
和#
或##
無關,所以前置處理器必須先展開NAME
對應的序列。因為bcopy
是一個類物件巨集(Object-like Macro),前置處理器會先將bcopy
展開為__bionic_bcopy
。之後代入__bionic_bcopy
得到BUILTIN(__bionic_bcopy)
。 - 因為
BUILTIN
也是一個類函式巨集,所以要進一步展開。 - 將
BUILTIN(NAME)
代換為BUILTIN_##NAME,
。因為NAME
的前面有##
所以就直接代入__bionic_bcopy
得到BUILTIN___bionic_bcopy,
。 - 因為
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
,再交由 STR
將 4
字串化。
阻止類函式巨集展開
有時候我們編寫 Header File 的時候,可能會用到一些常見的識別字(例如:std::min
或 std::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++ 前置處理器有進一步的認識。
參考資料
- C++ Programming Language Standard Draft (N4296), Section 16.3. Macro replacement
- C++ Programming Language Standard Draft (N4296), Section 16.3.4. Rescanning and further replacement
- GCC: The C Preprocessor, Section 3.4. Stringification
- Boost Library Requirements and Guidelines
- C++ Standard Core Language Active Issues, Revision 100, 268. Macro name suppression in rescanned replacement text
- The GNU C Preprocessor Internals, Macro Expansion Algorithm
致謝
感謝讀者 Stimim 指出巨集展開的過程中,部分巨集參照不會再次被代換。目前顯示的文章是更正後的版本。
[1] | 具體的例子是遞迴的巨集不會被再次展開: #define EXAMPLE(X) (EXAMPLE(X + 1) - EXAMPLE(1))
EXAMPLE(5)
如下所示,展開後 (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)
根據不同的解釋, |
Note
如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。