1. 程式人生 > 實用技巧 >《C/C++ 巨集程式設計的藝術》

《C/C++ 巨集程式設計的藝術》

C/C++ 巨集程式設計的藝術原文:《C/C++ 巨集程式設計的藝術》,公眾號BOTManJL~

可以言傳者,物之粗也;可以意致者,物之精也。——《莊子 · 秋水》

寫在前面

之前寫過幾篇關於 C/C++巨集(macro)和 C++超程式設計(metaprogramming)的文章:

C++模板超程式設計(template metaprogramming)雖然功能強大,但也有侷限性:

  • 不能通過 模板展開 生成新的識別符號(identifier)
  • 例如 生成新的 函式名、類名、名字空間名 等
  • 使用者 只能使用 預先定義的識別符號
  • 不能通過 模板引數 獲取符號 / 標記(token)的字面量(literal)
  • 例如 在反射中獲取 實參引數名的字面量,在斷言中獲取 表示式的字面量
  • 使用者 只能通過 傳遞字串引數 繞開

所以,在需要直接操作識別符號的情況下,還需要藉助巨集,進行預處理階段的超程式設計:

  • 和編譯時(compile-time)的模板展開不同,巨集在編譯前的預處理 (preprocess)
    階段全部展開 —— 狹義上,編譯器 看不到且不處理 巨集程式碼
  • 通過#define/TOKEN1##TOKEN2/#TOKEN定義巨集物件(object-like macro)和巨集函式(function-like macro),可以實現替換文字、拼接識別符號、獲取字面量等功能

最近,需要在單元測試中 自動生成呼叫gmock的程式碼:

  • 由於不便引入其他工具鏈,不能使用程式碼生成器(code generator)
  • 生成的程式碼 需要呼叫 gmock 的巨集函式,也不能使用 C++模板超程式設計
  • 所以,只能藉助巨集程式設計的魔法

本文使用的程式碼連結線上演示

如何除錯

介紹巨集程式設計之前,先聊聊除錯的問題。

很多人因為 “巨集程式設計” 無法除錯,而直接 “從入門到放棄” —— 不經意的符號拼寫錯誤、引數個數錯誤,導致文字不能正確替換,從而帶來滿屏的編譯錯誤,最後難以定位問題所在 ——

  • 最壞的情況下,編譯器只會告訴你cpp 檔案 編譯時出現語法錯誤
  • 最好的情況下,編譯器可能告訴你XXX 巨集 展開結果裡包含語法錯誤
  • 而永遠不會告訴你是因為 XXX 巨集展開成什麼樣,導致 YYY 巨集展開失敗
  • 最後只能看到ZZZ 巨集展開錯誤

由於巨集程式碼會 在編譯前全部展開,我們可以:

  • 讓編譯器僅輸出預處理結果
  • gcc -E讓編譯器 在預處理結束後停止,不進行 編譯、連結
  • gcc -P遮蔽編譯器 輸出預處理結果的行標記 (linemarker),減少干擾
  • 另外,由於輸出結果沒有格式化,建議先傳給clang-format格式化後再輸出
  • 遮蔽無關的標頭檔案
  • 臨時刪掉 不影響巨集展開的#include
  • 避免多餘的引用展開,導致實際關注的巨集程式碼 “被淹沒”

於是,展開錯誤一目瞭然(很容易發現_REMOVE_PARENS_IMPL的展開錯誤):

特殊符號

和模板超程式設計不一樣,巨集程式設計沒有型別的概念,輸入和輸出都是符號—— 不涉及編譯時的 C++ 語法,只進行編譯前的文字替換:

  • 一個巨集引數是一個任意的符號序列(token sequence),不同巨集引數之間 用逗號分隔
  • 每個引數可以是空序列,且空白字元會被忽略(例如a + 1a+1相同)
  • 在一個引數內,不能出現逗號(comma)或 不配對的括號(parenthesis)(例如FOO(bool, std::pair<int, int>)被認為是FOO()有三個引數:bool/std::pair<int/int>

如果需要把std::pair<int, int>作為一個引數,一種方法是使用 C++ 的類型別名(type alias)(例如using IntPair = std::pair<int, int>;),避免 引數中出現逗號(即FOO(bool, IntPair)只有兩個引數)。

更通用的方法是使用括號對封裝每個引數(下文稱為元組),並在最終展開時 移除括號(元組解包)即可:

#define PP_REMOVE_PARENS(T) PP_REMOVE_PARENS_IMPL T
#define PP_REMOVE_PARENS_IMPL(...) __VA_ARGS__

#define FOO(A, B) int foo(A x, B y)
#define BAR(A, B) FOO(PP_REMOVE_PARENS(A), PP_REMOVE_PARENS(B))

FOO(bool, IntPair)                  // -> int foo(bool x, IntPair y)
BAR((bool), (std::pair<int, int>))  // -> int foo(bool x, std::pair<int, int> y)
  • PP_REMOVE_PARENS(T)展開為PP_REMOVE_PARENS_IMPL T的形式
  • 如果引數T是一個括號對,那麼展開結果會變成呼叫巨集函式PP_REMOVE_PARENS_IMPL (...)的形式
  • 接著,PP_REMOVE_PARENS_IMPL(...)再展開為引數本身__VA_ARGS__(下文提到的變長引數),即元組T的內容

另外,常用巨集函式 代替特殊符號,用於下文提到的惰性求值:

#define PP_COMMA() ,
#define PP_LPAREN() (
#define PP_RPAREN() )
#define PP_EMPTY()

符號拼接

在巨集程式設計中,拼接識別符號(identifier concatenation / token pasting)通過##將巨集函式的引數 拼接成其他符號,再進一步 展開為目標結果,是巨集程式設計的實現基礎。

然而,如果一個巨集引數用於拼接識別符號(或獲取字面量),那麼它不會被展開(例如BAR()在拼接前不會展開為bar):

#define FOO(SYMBOL) foo_ ## SYMBOL
#define BAR() bar

FOO(bar)    // -> foo_bar
FOO(BAR())  // -> foo_BAR()

一種通用的方法是延遲拼接操作(或延遲 獲取字面量 操作):

#define PP_CONCAT(A, B) PP_CONCAT_IMPL(A, B)
#define PP_CONCAT_IMPL(A, B) A##B

#define FOO(N) PP_CONCAT(foo_, N)

FOO(bar)    // -> foo_bar
FOO(BAR())  // -> foo_bar
  • 在進入巨集函式前,所有巨集引數會先進行一次預掃描 (prescan),完全展開未用於拼接識別符號 或 獲取字面量 的所有引數
  • 在巨集函式展開時,用(預掃描展開後的)引數替換 展開目標裡的同名符號
  • 在巨集函式展開後,替換後的文字會進行二次掃描(scan twice),繼續展開 結果裡出現的巨集
  • 所以,PP_CONCAT()先展開引數,再傳遞給PP_CONCAT_IMPL()進行實際拼接

延伸閱讀:使用 C++ 巨集巢狀實現窄字元轉換為寬字元 by bingoli提到了Win32 的 TEXT() 巨集的原理。

另外,在預掃描前後,巨集函式都要求引數個數必須匹配,否則無法展開:

PP_CONCAT(x PP_COMMA() y)  // too few arguments (before prescan)
PP_CONCAT(x, PP_COMMA())   // too many arguments (after prescan)
  • 預掃描前,x PP_COMMA() y是一個引數
  • 預掃描後,x, PP_COMMA()是三個引數

自增自減

藉助PP_CONCAT(),我們可以實現非負整數增減(即INC(N) = N + 1/DEC(N) = N - 1):

#define PP_INC(N) PP_CONCAT(PP_INC_, N)
#define PP_INC_0 1
#define PP_INC_1 2
// ...
#define PP_INC_254 255
#define PP_INC_255 256

#define PP_DEC(N) PP_CONCAT(PP_DEC_, N)
#define PP_DEC_256 255
#define PP_DEC_255 254
// ...
#define PP_DEC_2 1
#define PP_DEC_1 0

PP_INC(1)    // -> 2
PP_DEC(2)    // -> 1
PP_INC(256)  // -> PP_INC_256 (overflow)
PP_DEC(0)    // -> PP_DEC_0  (underflow)
  • PP_INC(N)/PP_DEC(N)先展開為PP_INC_N/PP_DEC_N,再經過二次掃描展開為對應數值N + 1/N - 1的符號
  • 但上述操作有上限,若超出則無法繼續展開(例如BOOST_PP 數值操作的上限是 256

邏輯運算

藉助PP_CONCAT(),我們可以實現布林型別(01)的邏輯運算(與 / 或 / 非 / 異或 / 同或):

#define PP_NOT(N) PP_CONCAT(PP_NOT_, N)
#define PP_NOT_0 1
#define PP_NOT_1 0

#define PP_AND(A, B) PP_CONCAT(PP_AND_, PP_CONCAT(A, B))
#define PP_AND_00 0
#define PP_AND_01 0
#define PP_AND_10 0
#define PP_AND_11 1

PP_AND(PP_NOT(0), 1)  // -> 1
PP_AND(PP_NOT(2), 0)  // -> PP_AND_PP_NOT_20
  • 原理和PP_INC()/PP_DEC()類似(符號拼接 + 二次展開)
  • 但上述操作不支援非負整數的通用邏輯運算(僅支援01
  • 如果通過定義PP_NOT_2來支援PP_NOT(2),巨集程式碼會急劇膨脹
  • 一元運算PP_NOT()需要考慮 $N$ 種組合
  • 二元運算PP_AND()則要考慮 $N^2$ 種組合

布林轉換

為了支援更通用的非負整數的邏輯運算,可以先將整數 轉換成 布林型別,而不是擴展布爾型別的邏輯運算:

#define PP_BOOL(N) PP_CONCAT(PP_BOOL_, N)
#define PP_BOOL_0 0
#define PP_BOOL_1 1
#define PP_BOOL_2 1
// ...

PP_AND(PP_NOT(PP_BOOL(2)), PP_BOOL(0))  // -> 0
PP_NOT(PP_BOOL(1000))                   // -> PP_NOT_PP_BOOL_1000
  • 原理和PP_INC()/PP_DEC()類似(符號拼接 + 二次展開)
  • 同理,上述操作也有上限,若超出則無法繼續展開

條件選擇

藉助PP_CONCAT()PP_BOOL(),我們可以實現通用的條件選擇表示式(PRED ? THEN : ELSE,其中PRED可以是任意非負整數):

#define PP_IF(PRED, THEN, ELSE) PP_CONCAT(PP_IF_, PP_BOOL(PRED))(THEN, ELSE)
#define PP_IF_1(THEN, ELSE) THEN
#define PP_IF_0(THEN, ELSE) ELSE

#define DEC_SAFE(N) PP_IF(N, PP_DEC(N), 0)

DEC_SAFE(2)  // -> 1
DEC_SAFE(1)  // -> 0
DEC_SAFE(0)  // -> 0
  • PP_IF()先會根據轉換後的條件PP_BOOL(PRED)選擇PP_IF_1PP_IF_0符號
  • PP_IF_1()/PP_IF_0()接受相同的引數,但分別展開為THENELSE引數

惰性求值

需要注意PP_IF()的引數會在預掃描階段被完全展開(例如PP_COMMA()會被立即展開為逗號,導致引數個數錯誤):

#define PP_COMMA_IF(N) PP_IF(N, PP_COMMA(), PP_EMPTY())

PP_COMMA_IF(1)  // -> PP_IF(1, , , ) (too many arguments after prescan)

常用的技巧是惰性求值(lazy evaluation),即 條件選擇先返回巨集函式,再傳遞引數延遲呼叫:

#define PP_COMMA_IF(N) PP_IF(N, PP_COMMA, PP_EMPTY)()

PP_COMMA_IF(0)  // (empty)
PP_COMMA_IF(1)  // -> ,
PP_COMMA_IF(2)  // -> ,

#define SURROUND(N) PP_IF(N, PP_LPAREN, [ PP_EMPTY)() \
                    N                                 \
                    PP_IF(N, PP_RPAREN, ] PP_EMPTY)()

SURROUND(0)  // -> [0]
SURROUND(1)  // -> (1)
SURROUND(2)  // -> (2)
  • PP_COMMA_IF()先借助PP_IF()返回PP_COMMAPP_EMPTY符號
  • PP_COMMA/PP_EMPTY和後邊的括號對 組成PP_COMMA()/PP_EMPTY(),再繼續展開為逗號或空
  • 如果需要展開為其他符號SYMBOL,可以使用SYMBOL PP_EMPTY作為引數,和後邊的括號對 組成PP_EMPTY()(例如SURROUND()使用的[]

變長引數

從 C++ 11 開始,巨集函式支援了變長引數...,接受任意個巨集引數(用逗號分隔):

  • 傳入的變長引數可以用__VA_ARGS__獲取(也可以通過#__VA_ARGS__獲取 逗號 + 空格分隔 的引數字面量)
  • 另外,允許傳遞空引數,即__VA_ARGS__替換為空

對於空引數,展開時需要處理多餘逗號的問題:

#define log(format, ...) printf("LOG: " format, __VA_ARGS__)

log("%d%f", 1, .2);    // -> printf("LOG: %d%f", 1, .2);
log("hello world");    // -> printf("LOG: hello world", );
log("hello world", );  // -> printf("LOG: hello world", );
  • 後兩種呼叫 分別對應不傳變長引數、變長引數為空的情況
  • 展開結果會 多出一個逗號,導致 C/C++編譯錯誤(而不是 巨集展開錯誤)

為了解決這個問題,一些編譯器(例如 gcc/clang)擴充套件了, ## __VA_ARGS__的用法 —— 如果不傳變長引數,則省略前面的逗號:

#define log(format, ...) printf("LOG: " format, ## __VA_ARGS__)

log("%d%f", 1, .2);    // -> printf("LOG: %d%f", 1, .2);
log("hello world");    // -> printf("LOG: hello world");
log("hello world", );  // -> printf("LOG: hello world", );

為了進一步處理變長引數為空的情況,C++ 20 引入了__VA_OPT__識別符號 —— 如果變長引數是空引數,不展開該符號(不僅限於逗號):

#define log(format, ...) printf("LOG: " format __VA_OPT__(,) __VA_ARGS__)

log("%d%f", 1, .2);    // -> printf("LOG: %d%f", 1, .2);
log("hello world");    // -> printf("LOG: hello world");
log("hello world", );  // -> printf("LOG: hello world");

下文將藉助長度判空和遍歷訪問,實現__VA_OPT__(,)的功能。

下標訪問

藉助PP_CONCAT(),我們可以通過下標訪問變長引數的特定元素:

#define PP_GET_N(N, ...) PP_CONCAT(PP_GET_N_, N)(__VA_ARGS__)
#define PP_GET_N_0(_0, ...) _0
#define PP_GET_N_1(_0, _1, ...) _1
#define PP_GET_N_2(_0, _1, _2, ...) _2
// ...
#define PP_GET_N_8(_0, _1, _2, _3, _4, _5, _6, _7, _8, ...) _8

PP_GET_N(0, foo, bar)  // -> foo
PP_GET_N(1, foo, bar)  // -> bar
  • PP_GET_N()的引數分為兩部分:下標N和 變長引數...
  • 先通過PP_CONCAT()選擇下標I(從0開始)對應的PP_GET_N_I符號
  • PP_GET_N_I()接受至少I + 1個引數(其餘的引數是變長引數),並返回第I + 1個引數(其餘的變長引數直接丟棄)

藉助PP_REMOVE_PARENS(),我們還可以通過 下標訪問元組的特定元素:

#define PP_GET_TUPLE(N, T) PP_GET_N(N, PP_REMOVE_PARENS(T))

PP_GET_TUPLE(0, (foo, bar))  // -> foo
PP_GET_TUPLE(1, (foo, bar))  // -> bar

需要注意變長引數的長度必須大於N,否則無法展開:

#define FOO(P, T) PP_IF(P, PP_GET_TUPLE(1, T), PP_GET_TUPLE(0, T))

FOO(0, (foo, bar))  // -> foo
FOO(1, (foo, bar))  // -> bar
FOO(0, (baz))       // -> PP_GET_N_1(baz) (too few arguments)
  • 對於P == 0的情況,FOO()只返回T的第一個元素
  • 但是另一個分支裡的PP_GET_TUPLE(1, T)仍會被展開,從而要求T有至少兩個元素

類似的,我們可以藉助惰性求值避免該問題:

#define FOO(P, T) PP_IF(P, PP_GET_N_1, PP_GET_N_0) T

FOO(0, (foo, bar))  // -> foo
FOO(1, (foo, bar))  // -> bar
FOO(0, (baz))       // -> baz
  • PP_IF()先返回PP_GET_N_1PP_GET_N_0符號
  • 類似PP_REMOVE_PARENS(),再用PP_GET_N_I (...)元組解包
  • 對於P == 0的情況,不會展開PP_GET_N_1()巨集

長度判空

藉助PP_GET_N(),我們可以檢查變長引數是否為空:

#define PP_IS_EMPTY(...)                                      \
  PP_AND(PP_AND(PP_NOT(PP_HAS_COMMA(__VA_ARGS__)),            \
                PP_NOT(PP_HAS_COMMA(__VA_ARGS__()))),         \
         PP_AND(PP_NOT(PP_HAS_COMMA(PP_COMMA_V __VA_ARGS__)), \
                PP_HAS_COMMA(PP_COMMA_V __VA_ARGS__())))
#define PP_HAS_COMMA(...) PP_GET_N_8(__VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 0, 0)
#define PP_COMMA_V(...) ,

PP_IS_EMPTY()          // -> 1
PP_IS_EMPTY(foo)       // -> 0
PP_IS_EMPTY(foo())     // -> 0
PP_IS_EMPTY(())        // -> 0
PP_IS_EMPTY(()foo)     // -> 0
PP_IS_EMPTY(PP_EMPTY)  // -> 0
PP_IS_EMPTY(PP_COMMA)  // -> 0
PP_IS_EMPTY(, )        // -> 0
PP_IS_EMPTY(foo, bar)  // -> 0
PP_IS_EMPTY(, , , )    // -> 0
  • 先定義兩個輔助巨集:
  • PP_HAS_COMMA()用於檢查變長引數裡有沒有逗號(原理類似下文的PP_NARG()
  • PP_COMMA_V()用於吃掉(eat)變長引數,並返回一個逗號
  • 如果變長引數為空,需要滿足以下條件:
  • PP_COMMA_V __VA_ARGS__()展開為逗號,即構成PP_COMMA_V()的形式
  • __VA_ARGS____VA_ARGS__()PP_COMMA_V __VA_ARGS__展開結果裡 沒有逗號,排除對上一個條件的干擾

藉助PP_COMMA_IF()PP_IS_EMPTY(),我們可以實現 C++ 20 的__VA_OPT__(,)功能:

#define PP_VA_OPT_COMMA(...) PP_COMMA_IF(PP_NOT(PP_IS_EMPTY(__VA_ARGS__)))
#define log(format, ...) \
  printf("LOG: " format PP_VA_OPT_COMMA(__VA_ARGS__) __VA_ARGS__)

log("%d%f", 1, .2);    // -> printf("LOG: %d%f", 1, .2);
log("hello world");    // -> printf("LOG: hello world");
log("hello world", );  // -> printf("LOG: hello world");

長度計算

藉助PP_GET_N()PP_VA_OPT_COMMA(),我們可以計算變長引數的個數(長度):

#define PP_NARG(...)                                                           \
  PP_GET_N(8, __VA_ARGS__ PP_VA_OPT_COMMA(__VA_ARGS__) 8, 7, 6, 5, 4, 3, 2, 1, \
           0)

PP_NARG()          // -> 0
PP_NARG(foo)       // -> 1
PP_NARG(foo())     // -> 1
PP_NARG(())        // -> 1
PP_NARG(()foo)     // -> 1
PP_NARG(PP_EMPTY)  // -> 1
PP_NARG(PP_COMMA)  // -> 1
PP_NARG(, )        // -> 2
PP_NARG(foo, bar)  // -> 2
PP_NARG(, , , )    // -> 4
  • __VA_ARGS__ PP_VA_OPT_COMMA(__VA_ARGS__)8, ..., 0一起傳給PP_GET_N(8, ...)
  • 如果__VA_ARGS__為空,等價與PP_GET_N(8, 8, ..., 0),直接返回 第九個元素0
  • 如果__VA_ARGS__非空,等價於PP_GET_N(8, __VA_ARGS__, 8, ..., 0),變長引數__VA_ARGS__8, ..., 0向後推移,使得返回的 第九個元素 剛好是__VA_ARGS__的引數個數
  • 然而,上述操作有上限(例如 此處支援的最大長度為8

另外,這裡只能用PP_GET_N(8, ...),而不能用PP_GET_N_8()

PP_GET_N(0, 1 PP_COMMA() 2)  // -> 1
PP_GET_N_0(1 PP_COMMA() 2)   // -> 1 , 2
  • 如果使用PP_GET_N_8(),沒被展開的__VA_ARGS__ PP_VA_OPT_COMMA(__VA_ARGS__) 8會被當成包含逗號的一個引數,而不是 多個引數
  • PP_GET_N()在把__VA_ARGS__轉發給PP_GET_N_8()時,會把 上述引數 展開為 多個引數

遍歷訪問

藉助PP_CONCAT()PP_NARG(),我們可以遍歷(traverse)變長引數:

#define PP_FOR_EACH(DO, CTX, ...) \
  PP_CONCAT(PP_FOR_EACH_, PP_NARG(__VA_ARGS__))(DO, CTX, 0, __VA_ARGS__)
#define PP_FOR_EACH_0(DO, CTX, IDX, ...)
#define PP_FOR_EACH_1(DO, CTX, IDX, VAR, ...) DO(VAR, IDX, CTX)
#define PP_FOR_EACH_2(DO, CTX, IDX, VAR, ...) \
  DO(VAR, IDX, CTX)                           \
  PP_FOR_EACH_1(DO, CTX, PP_INC(IDX), __VA_ARGS__)
#define PP_FOR_EACH_3(DO, CTX, IDX, VAR, ...) \
  DO(VAR, IDX, CTX)                           \
  PP_FOR_EACH_2(DO, CTX, PP_INC(IDX), __VA_ARGS__)
// ...

#define DO_EACH(VAR, IDX, CTX) PP_COMMA_IF(IDX) CTX VAR

PP_FOR_EACH(DO_EACH, void, )        // (empty)
PP_FOR_EACH(DO_EACH, int, a, b, c)  // -> int a, int b, int c
PP_FOR_EACH(DO_EACH, bool, x)       // -> bool x
  • PP_FOR_EACH()的引數分為三部分:元素的轉換操作DO、遍歷的上下文引數CTX和 變長引數...
  • 其中DO()接受三個引數:當前元素VAR、對應下標IDX和 遍歷的上下文CTX,並返回元素VAR轉換後的結果
  • 先通過PP_CONCAT()PP_NARG()選擇 變長引數長度 對應的PP_FOR_EACH_I符號
  • PP_FOR_EACH_I()的引數分為四部分:元素的轉換操作DO、遍歷的上下文引數CTX、當前元素下標IDX和 變長引數...
  • 展開為兩部分:變長引數第一個元素的轉換DO()和 變長引數剩餘元素遞迴呼叫I - 1巨集(下標更新為IDX + 1
  • I == 0時,展開為空,遞迴終止

藉助PP_FOR_EACH()和 上邊的DO_EACH()(藉助其PP_COMMA_IF(),並忽略CTX),我們可以實現等效於PP_VA_OPT_COMMA()的功能:

#define log(format, ...) \
  printf("LOG: " format PP_FOR_EACH(DO_EACH, , __VA_ARGS__))

log("%d%f", 1, .2);    // -> printf("LOG: %d%f", 1, .2);
log("hello world");    // -> printf("LOG: hello world");
log("hello world", );  // -> printf("LOG: hello world");

符號匹配

藉助PP_CONCAT()PP_IS_EMPTY(),我們可以匹配任意的特定符號:

#define PP_IS_SYMBOL(PREFIX, SYMBOL) PP_IS_EMPTY(PP_CONCAT(PREFIX, SYMBOL))
#define IS_VOID_void

PP_IS_SYMBOL(IS_VOID_, void)            // -> 1
PP_IS_SYMBOL(IS_VOID_, )                // -> 0
PP_IS_SYMBOL(IS_VOID_, int)             // -> 0
PP_IS_SYMBOL(IS_VOID_, void*)           // -> 0
PP_IS_SYMBOL(IS_VOID_, void x)          // -> 0
PP_IS_SYMBOL(IS_VOID_, void(int, int))  // -> 0
  • 先定義一個輔助巨集IS_VOID_void:字面量是字首IS_VOID_和 目標結果void的拼接,展開為空
  • 再通過PP_CONCAT(PREFIX, SYMBOL)把 字首 和 引數 拼接為新的符號,並用PP_IS_EMPTY()檢查拼接結果 展開後是否為空
  • 只有SYMBOL是單個符號void,才能展開為空
  • 但該方法不支援模式匹配(如果大家有什麼好想法,歡迎提出~)

藉助PP_IS_EMPTY(),我們還可以檢查符號序列 是否是元組:

#define PP_EMPTY_V(...)
#define PP_IS_PARENS(SYMBOL) PP_IS_EMPTY(PP_EMPTY_V SYMBOL)

PP_IS_PARENS()                // -> 0
PP_IS_PARENS(foo)             // -> 0
PP_IS_PARENS(foo())           // -> 0
PP_IS_PARENS(()foo)           // -> 0
PP_IS_PARENS(())              // -> 1
PP_IS_PARENS((foo))           // -> 1
PP_IS_PARENS(((), foo, bar))  // -> 1
  • 先定義一個輔助巨集PP_EMPTY_V():用於吃掉變長引數,展開為空
  • 再通過PP_IS_EMPTY()檢查PP_EMPTY_V SYMBOL拼接結果 展開後是否為空
  • 只有SYMBOL符合(...)的形式,PP_EMPTY_V (...)才能展開為空

gmock-1.10.0中,MOCK_METHOD()藉助PP_IS_PARENS(),自動識別引數是不是元組,再進行 選擇性的元組解包—— 使用時可以只把 包含逗號的引數 變為元組,而其他引數保持不變:

#define PP_IDENTITY(N) N
#define TRY_REMOVE_PARENS(T) \
  PP_IF(PP_IS_PARENS(T), PP_REMOVE_PARENS, PP_IDENTITY)(T)

#define FOO(A, B) int foo(A x, B y)
#define BAR(A, B) FOO(TRY_REMOVE_PARENS(A), TRY_REMOVE_PARENS(B))

FOO(bool, IntPair)                // -> int foo(bool x, IntPair y)
BAR(bool, IntPair)                // -> int foo(bool x, IntPair y)
BAR(bool, (std::pair<int, int>))  // -> int foo(bool x, std::pair<int, int> y)

資料結構

由於變長引數只能表示一維資料,如果需要處理巢狀的多維資料,還需要高階的資料結構(例如 列表的每一項 包含多個屬性,而每個屬性 又是一個列表;參考 下文的遞迴重入提到的巢狀元組)。

BOOST_PP 定義了四種資料結構:

  • 元組 (tuple)的每個元素 通過逗號分隔,所有元素放到一個括號對裡
  • 序列 (sequence)的每個元素 放到一個元組裡,組成多個連續的元組
  • 列表 (list)是一個遞迴定義的二元組,第一個元素是 當前元素,第二個元素是 後續列表,並通過nil標識結束符
  • 陣列 (array)=元組實際長度 + 元組組成的二元組(已過時,直接使用元組即可)

例如,一組資料的三個元素 分別是f(12)/a + 1/foo

  • 元組 表示為(f(12), a + 1, foo)
  • 序列 表示為(f(12))(a + 1)(foo)
  • 列表 表示為(f(12), (a + 1, (foo, PP_NIL)))
  • 陣列 表示為(3, (f(12), a + 1, foo))

另外,元組()表示 包含一個空元素的一元組,而不是 不包含任何元素的空元組(序列、列表、陣列 不涉及這個問題)。

關於上述資料結構的基本運算(下標訪問、長度計算、遍歷訪問、增刪元素、型別轉換),推薦閱讀BOOST_PP 原始碼

遞迴重入

因為自參照巨集 (self referential macro)不會被展開 —— 在展開一個巨集時,如果遇到 當前巨集 的符號,則不會繼續展開,避免無限展開(infinite expansion)—— 所以巨集不支援 遞迴 / 重入。

例如,PP_FOR_EACH()在遍歷兩層巢狀元組時,DO_EACH_1()無法展開內層元組,結果保留PP_FOR_EACH(...)的形式:

#define OUTER(N, T) PP_FOR_EACH(DO_EACH_1, N, PP_REMOVE_PARENS(T))
#define DO_EACH_1(VAR, IDX, CTX)                   \
  PP_FOR_EACH(DO_EACH_2, CTX.PP_GET_TUPLE(0, VAR), \
              PP_REMOVE_PARENS(PP_GET_TUPLE(1, VAR)))
#define DO_EACH_2(VAR, IDX, CTX) CTX .VAR = VAR;

// -> PP_FOR_EACH(DO_EACH_2, obj.x, x1, x2) PP_FOR_EACH(DO_EACH_2, obj.y, y1)
OUTER(obj, ((x, (x1, x2)), (y, (y1))))

一種解決方法是,在預掃描階段,先展開內層元組,再把展開結果作為引數,傳遞給外層元組,從而避免 遞迴呼叫(但不一定適用於所有場景):

#define OUTER(N, T) PP_FOR_EACH(DO_EACH_1, N, PP_REMOVE_PARENS(T))
#define DO_EACH_1(VAR, IDX, CTX) CTX.VAR;
#define INNER(N, T) PP_FOR_EACH(DO_EACH_2, N, PP_REMOVE_PARENS(T))
#define DO_EACH_2(VAR, IDX, CTX) PP_COMMA_IF(IDX) CTX .VAR = VAR

// -> obj.x.x1 = x1; obj.x.x2 = x2; obj.y.y1 = y1;
OUTER(obj, (INNER(x, (x1, x2)), INNER(y, (y1))))

另一種解決方法是,定義另一個相同功能的巨集PP_FOR_EACH_INNER(),用於內層迴圈,從而避免和外層迴圈衝突(如果遍歷三層巢狀,則需要再定義一個類似的巨集):

#define PP_FOR_EACH_INNER(DO, CTX, ...)               \
  PP_CONCAT(PP_FOR_EACH_INNER_, PP_NARG(__VA_ARGS__)) \
  (DO, CTX, 0, __VA_ARGS__)
#define PP_FOR_EACH_INNER_0(DO, CTX, IDX, ...)
#define PP_FOR_EACH_INNER_1(DO, CTX, IDX, VAR, ...) DO(VAR, IDX, CTX)
#define PP_FOR_EACH_INNER_2(DO, CTX, IDX, VAR, ...) \
  DO(VAR, IDX, CTX)                                 \
  PP_FOR_EACH_INNER_1(DO, CTX, PP_INC(IDX), __VA_ARGS__)
// ...

#define OUTER(N, T) PP_FOR_EACH(DO_EACH_1, N, PP_REMOVE_PARENS(T))
#define DO_EACH_1(VAR, IDX, CTX)                         \
  PP_FOR_EACH_INNER(DO_EACH_2, CTX.PP_GET_TUPLE(0, VAR), \
                    PP_REMOVE_PARENS(PP_GET_TUPLE(1, VAR)))
#define DO_EACH_2(VAR, IDX, CTX) CTX .VAR = VAR;

// -> obj.x.x1 = x1; obj.x.x2 = x2; obj.y.y1 = y1;
OUTER(obj, ((x, (x1, x2)), (y, (y1))))

條件迴圈

上文提到的PP_FOR_EACH()主要用於遍歷變長引數的元素,輸出長度和輸入相同。但有時候,我們仍需要一個用於迭代(iterate)的條件迴圈PP_WHILE(),最後只輸出一個結果:

#define PP_WHILE PP_WHILE_1
#define PP_WHILE_1(PRED, OP, VAL)              \
  PP_IF(PRED(VAL), PP_WHILE_2, VAL PP_EMPTY_V) \
  (PRED, OP, PP_IF(PRED(VAL), OP, PP_EMPTY_V)(VAL))
#define PP_WHILE_2(PRED, OP, VAL)              \
  PP_IF(PRED(VAL), PP_WHILE_3, VAL PP_EMPTY_V) \
  (PRED, OP, PP_IF(PRED(VAL), OP, PP_EMPTY_V)(VAL))
#define PP_WHILE_3(PRED, OP, VAL)              \
  PP_IF(PRED(VAL), PP_WHILE_4, VAL PP_EMPTY_V) \
  (PRED, OP, PP_IF(PRED(VAL), OP, PP_EMPTY_V)(VAL))
#define PP_WHILE_4(PRED, OP, VAL)              \
  PP_IF(PRED(VAL), PP_WHILE_5, VAL PP_EMPTY_V) \
  (PRED, OP, PP_IF(PRED(VAL), OP, PP_EMPTY_V)(VAL))
// ...

#define PRED(VAL) PP_GET_TUPLE(1, VAL)
#define OP(VAL) \
  (PP_GET_TUPLE(0, VAL) + PP_GET_TUPLE(1, VAL), PP_DEC(PP_GET_TUPLE(1, VAL)))

PP_GET_TUPLE(0, PP_WHILE(PRED, OP, (x, 2)))  // -> x + 2 + 1
  • PP_WHILE()接受三個引數:迴圈條件謂詞PRED、迭代操作運算OP和 初始值VAL
  • 其中PRED()接受 當前值VAL作為引數,並返回 非負整數
  • 其中OP()接受 當前值VAL作為引數,並返回 迭代後的下一個VAL
  • 原理和PP_FOR_EACH()類似,PP_WHILE_I()根據PRED(VAL)選擇展開方式
  • 如果PRED(VAL) != 0,遞迴呼叫I + 1巨集,並傳入OP(VAL)作為下一輪迭代的 當前值
  • 如果PRED(VAL) == 0,展開為VAL,並跳過OP(VAL),遞迴終止
  • PP_WHILEPP_WHILE_1開始迭代

PP_FOR_EACH()不同,不需要定義PP_WHILE_INNER(),就可以在迴圈展開時重入 —— 如果當前遞迴狀態是I,重入程式碼可以使用 任意I以後的巨集:

  • 例如 當展開PP_WHILE_2()時,只有PP_WHILE_1PP_WHILE_2正在展開,所以PRED()/OP()可以使用PP_WHILE_3()及以後的巨集
  • 由於PRED(VAL)/OP(VAL)只在引數裡展開,在下一輪迭代的PP_WHILE_3()展開時,不會構成遞迴呼叫

為了支援方便的遞迴呼叫,BOOST_PP 提出了自動推導當前遞迴狀態的方法:

#define PP_WHILE PP_CONCAT(PP_WHILE_, PP_AUTO_DIM(PP_WHILE_CHECK))

#define PP_AUTO_DIM(CHECK) \
  PP_IF(CHECK(2), PP_AUTO_DIM_12, PP_AUTO_DIM_34)(CHECK)
#define PP_AUTO_DIM_12(CHECK) PP_IF(CHECK(1), 1, 2)
#define PP_AUTO_DIM_34(CHECK) PP_IF(CHECK(3), 3, 4)

#define PP_WHILE_CHECK(N) \
  PP_CONCAT(PP_WHILE_CHECK_, PP_WHILE_##N(0 PP_EMPTY_V, , 1))
#define PP_WHILE_CHECK_1 1
#define PP_WHILE_CHECK_PP_WHILE_1(PRED, OP, VAL) 0
#define PP_WHILE_CHECK_PP_WHILE_2(PRED, OP, VAL) 0
#define PP_WHILE_CHECK_PP_WHILE_3(PRED, OP, VAL) 0
#define PP_WHILE_CHECK_PP_WHILE_4(PRED, OP, VAL) 0
// ...

#define OP_1(VAL)                                                        \
  (PP_GET_TUPLE(0, PP_WHILE(PRED, OP_2,                                  \
                            (PP_GET_TUPLE(0, VAL), PP_GET_TUPLE(1, VAL), \
                             PP_GET_TUPLE(1, VAL)))),                    \
   PP_DEC(PP_GET_TUPLE(1, VAL)))
#define OP_2(VAL)                                                      \
  (PP_GET_TUPLE(0, VAL) + PP_GET_TUPLE(2, VAL) * PP_GET_TUPLE(1, VAL), \
   PP_DEC(PP_GET_TUPLE(1, VAL)), PP_GET_TUPLE(2, VAL))

PP_GET_TUPLE(0, PP_WHILE(PRED, OP_1, (x, 2)))  // -> x + 2 * 2 + 2 * 1 + 1 * 1
  • 定義輔助巨集PP_WHILE_CHECK(I)用於檢查I對應的PP_WHILE_I()是否可用
  • 使用0 PP_EMPTY_V作為謂詞,呼叫PP_WHILE_I()
  • 如果PP_WHILE_I()正在展開,此處不會再被展開,和字首PP_WHILE_CHECK_拼接為PP_WHILE_CHECK_PP_WHILE_I(0 PP_EMPTY_V, , 1)的形式,最後展開為0
  • 如果PP_WHILE_I()沒有使用,此處先被展開為1,再和字首PP_WHILE_CHECK_拼接為PP_WHILE_CHECK_1的形式,最後展開為1
  • 定義輔助巨集PP_AUTO_DIM()用於推導最小可用的遞迴狀態I
  • 使用二分查詢(binary search)的方法,時間複雜度可以降到 $O(log_{2}n)$
  • 假設下標最大值是4,那麼先檢查2是否可用;如果可用再嘗試1,否則檢查3
  • PP_WHILE通過PP_AUTO_DIM(PP_WHILE_CHECK)推匯出的PP_WHILE_I保證總是可用

不過,在展開PP_WHILE()時,當前遞迴狀態總是確定的,實際上不需要推導。所以 BOOST_PP 建議儘量傳遞狀態,而不是自動推導

  • PP_WHILE_I()展開時,把下一個狀態的下標I + 1(連同當前VAL)傳給PRED(PP_INC(I), VAL)OP(PP_INC(I), VAL)
  • PRED()/OP()可以直接使用I + 1對應的巨集(及I + 1以後的巨集),無需再用PP_AUTO_DIM()推導可用的下標

當然,自動推導和傳遞狀態也可以用於實現PP_FOR_EACH()的遞迴重入:

  • 先將PP_FOR_EACH定義為PP_AUTO_DIM(PP_FOR_EACH_CHECK)推匯出的PP_FOR_EACH_D符號(自動推導)
  • 每組PP_FOR_EACH_D再定義 不同變長引數個數I對應的PP_FOR_EACH_D_I,然後用 上文提到的方法 遍歷所有引數
  • 在展開DO()時,可以額外傳遞下一個狀態的下標D + 1(傳遞狀態)
  • BOOST_PP 支援 3 層迴圈巢狀每層迴圈可以遍歷 256 個變長引數,需要定義 3×256 個PP_FOR_EACH_D_I過載

延遲展開

CHAOS_PP 提出了一種基於延遲展開的遞迴呼叫方法

#define PP_WHILE_RECURSIVE(PRED, OP, VAL)          \
  PP_IF(PRED(VAL), PP_WHILE_DEFER, VAL PP_EMPTY_V) \
  (PRED, OP, PP_IF(PRED(VAL), OP, PP_EMPTY_V)(VAL))
#define PP_WHILE_INDIRECT() PP_WHILE_RECURSIVE
#define PP_WHILE_DEFER PP_WHILE_INDIRECT PP_EMPTY PP_EMPTY PP_EMPTY()()()()

// -> PP_WHILE_INDIRECT PP_EMPTY PP_EMPTY()()()
PP_WHILE_DEFER
// -> PP_WHILE_INDIRECT PP_EMPTY()()
PP_IDENTITY(PP_WHILE_DEFER)
// -> PP_WHILE_INDIRECT ()
PP_IF(1, PP_WHILE_DEFER, )
// -> PP_WHILE_RECURSIVE
PP_IDENTITY(PP_IF(1, PP_WHILE_DEFER, ))
  • PP_WHILE_I()類似,PP_WHILE_RECURSIVE()PRED(VAL) != 0的情況下,展開為呼叫PP_WHILE_DEFER巨集(即PP_WHILE_INDIRECT PP_EMPTY PP_EMPTY PP_EMPTY()()()())的形式
  • 其中的PP_EMPTY()起到了延遲展開的作用
  • PP_WHILE_DEFER會被原地展開為PP_WHILE_INDIRECT PP_EMPTY PP_EMPTY()()(),即其中一組PP_EMPTY()展開為空,然後停止展開
  • PP_WHILE_DEFER作為引數傳給PP_IF()時,一組PP_EMPTY()再展開為空;再作為PP_IF()的結果傳出時,一組PP_EMPTY()又展開為空;最後得到PP_WHILE_INDIRECT(),然後停止展開
  • 所以,在當前場景下,需要至少 3 組PP_EMPTY()
  • PP_WHILE_RECURSIVE()展開時
  • 如果PP_WHILE_DEFER內的PP_EMPTY()數量不足,就不會形成PP_WHILE_INDIRECT(),而直接變為PP_WHILE_RECURSIVE
  • 然而,自參照的巨集符號PP_WHILE_RECURSIVE不能繼續展開,即使使用下文提到的PP_EXPAND()也不行

在每次迴圈結束後,得到的PP_WHILE_INDIRECT(),需要先手動展開為PP_WHILE_RECURSIVE,再進入下一輪迭代,直到PRED(VAL) == 0為止:

#define PP_EXPAND(...) __VA_ARGS__

// -> PP_WHILE_INDIRECT() (PRED, OP, (x + 2, 1))
PP_WHILE_RECURSIVE(PRED, OP, (x, 2))
// -> PP_WHILE_INDIRECT() (PRED, OP, (x + 2 + 1, 0))
PP_EXPAND(PP_WHILE_RECURSIVE(PRED, OP, (x, 2)))
// -> (x + 2 + 1, 0)
PP_EXPAND(PP_EXPAND(PP_WHILE_RECURSIVE(PRED, OP, (x, 2))))
  • 需要展開幾輪PP_WHILE_RECURSIVE(),就需要巢狀幾次PP_EXPAND()
  • 所以,可以定義一個巢狀層數為最大迴圈次數的輔助巨集,專門用於PP_WHILE_RECURSIVE()的延遲展開機制

需要注意上述方法 不一定適用於所有編譯器,一般建議使用PP_WHILE()

數值運算

藉助PP_WHILE()PP_INC()/PP_DEC(),我們可以實現非負整數加法:

#define PP_ADD(X, Y) PP_GET_TUPLE(0, PP_WHILE(PP_ADD_P, PP_ADD_O, (X, Y)))
#define PP_ADD_P(V) PP_GET_TUPLE(1, V)
#define PP_ADD_O(V) (PP_INC(PP_GET_TUPLE(0, V)), PP_DEC(PP_GET_TUPLE(1, V)))

PP_ADD(0, 2)  // -> 2
PP_ADD(1, 1)  // -> 2
PP_ADD(2, 0)  // -> 2
  • PP_ADD()從二元組(X, Y)開始迭代
  • 迭代操作PP_ADD_O()返回(X + 1, Y - 1)
  • 終止條件PP_ADD_P()Y == 0,此時的X為所求(可能上溢)

藉助PP_WHILE()PP_DEC(),我們還可以實現非負整數減法:

#define PP_SUB(X, Y) PP_GET_TUPLE(0, PP_WHILE(PP_SUB_P, PP_SUB_O, (X, Y)))
#define PP_SUB_P(V) PP_GET_TUPLE(1, V)
#define PP_SUB_O(V) (PP_DEC(PP_GET_TUPLE(0, V)), PP_DEC(PP_GET_TUPLE(1, V)))

PP_SUB(2, 2)  // -> 0
PP_SUB(2, 1)  // -> 1
PP_SUB(2, 0)  // -> 2
  • PP_SUB()從二元組(X, Y)開始迭代
  • 迭代操作PP_SUB_O()返回(X - 1, Y - 1)
  • 終止條件PP_SUB_P()Y == 0,此時的X為所求(可能下溢)

藉助PP_WHILE()PP_ADD(),我們可以實現非負整數乘法:

#define PP_MUL(X, Y) PP_GET_TUPLE(0, PP_WHILE(PP_MUL_P, PP_MUL_O, (0, X, Y)))
#define PP_MUL_P(V) PP_GET_TUPLE(2, V)
#define PP_MUL_O(V)                                                    \
  (PP_ADD(PP_GET_TUPLE(0, V), PP_GET_TUPLE(1, V)), PP_GET_TUPLE(1, V), \
   PP_DEC(PP_GET_TUPLE(2, V)))

PP_MUL(1, 2)  // -> 2
PP_MUL(2, 1)  // -> 2
PP_MUL(2, 0)  // -> 0
PP_MUL(0, 2)  // -> 0
  • PP_MUL()從三元組(R, X, Y)開始迭代(R初始值為0
  • 迭代操作PP_MUL_O()返回(R + X, X, Y - 1)(此處的PP_ADD()內部呼叫PP_WHILE()巨集,構成遞迴重入)
  • 終止條件PP_MUL_P()Y == 0,此時的R為所求(可能上溢)

除法和取模運算基於數值比較,見下文。

數值比較

藉助PP_WHILE()PP_DEC(),我們還可以實現等於比較:

#define PP_CMP(X, Y) PP_WHILE(PP_CMP_P, PP_CMP_O, (X, Y))
#define PP_CMP_P(V) \
  PP_AND(PP_BOOL(PP_GET_TUPLE(0, V)), PP_BOOL(PP_GET_TUPLE(1, V)))
#define PP_CMP_O(V) (PP_DEC(PP_GET_TUPLE(0, V)), PP_DEC(PP_GET_TUPLE(1, V)))

#define PP_EQUAL(X, Y) PP_IDENTITY(PP_EQUAL_IMPL PP_CMP(X, Y))
#define PP_EQUAL_IMPL(RX, RY) PP_AND(PP_NOT(PP_BOOL(RX)), PP_NOT(PP_BOOL(RY)))

PP_EQUAL(1, 2)  // -> 0
PP_EQUAL(1, 1)  // -> 1
PP_EQUAL(1, 0)  // -> 0
  • PP_CMP()從二元組(X, Y)開始迭代
  • 迭代操作PP_CMP_O()返回(X - 1, Y - 1)(同PP_SUB_O()
  • 終止條件PP_CMP_P()X == 0 || Y == 0,此時的(X, Y)為所求(不會下溢)
  • 最終結果(RX, RY)只有三種情況:RX == 0 && RY == 0/RX != 0 && RY == 0/RX == 0 && RY != 0
  • PP_EQUAL()返回RX == 0 && RY == 0的布林值
  • 類似PP_WHILE_RECURSIVE()PP_EQUAL_IMPL PP_CMP(X, Y)PP_CMP()展開為(RX, RY)後,仍需要藉助PP_IDENTITY()手動展開PP_EQUAL_IMPL(RX, RY)

類似的,我們還可以實現小於比較:

#define PP_LESS(X, Y) PP_IDENTITY(PP_LESS_IMPL PP_CMP(X, Y))
#define PP_LESS_IMPL(RX, RY) PP_AND(PP_NOT(PP_BOOL(RX)), PP_BOOL(RY))

PP_LESS(0, 1)  // -> 1
PP_LESS(1, 2)  // -> 1
PP_LESS(1, 1)  // -> 0
PP_LESS(2, 1)  // -> 0
  • 藉助PP_CMP()的結果,PP_LESS()返回RX == 0 && RY != 0的布林值

其他比較方式(不等於、大於、小於等於、大於等於)可以通過PP_EQUAL()/PP_LESS()的 布林運算 得到。

藉助PP_IF()PP_LESS(),我們可以獲取最大值 / 最小值:

#define PP_MIN(X, Y) PP_IF(PP_LESS(X, Y), X, Y)
#define PP_MAX(X, Y) PP_IF(PP_LESS(X, Y), Y, X)

PP_MIN(0, 1)  // -> 0
PP_MIN(1, 1)  // -> 1
PP_MAX(1, 2)  // -> 2
PP_MAX(2, 1)  // -> 2

藉助PP_WHILE()PP_SUB()/PP_LESS(),我們可以實現非負整數除法 / 取模:

#define PP_DIV_BASE(X, Y) PP_WHILE(PP_DIV_BASE_P, PP_DIV_BASE_O, (0, X, Y))
#define PP_DIV_BASE_P(V) \
  PP_NOT(PP_LESS(PP_GET_TUPLE(1, V), PP_GET_TUPLE(2, V)))  // X >= Y
#define PP_DIV_BASE_O(V)                                                       \
  (PP_INC(PP_GET_TUPLE(0, V)), PP_SUB(PP_GET_TUPLE(1, V), PP_GET_TUPLE(2, V)), \
   PP_GET_TUPLE(2, V))

#define PP_DIV(X, Y) PP_GET_TUPLE(0, PP_DIV_BASE(X, Y))
#define PP_MOD(X, Y) PP_GET_TUPLE(1, PP_DIV_BASE(X, Y))

PP_DIV(2, 1), PP_MOD(2, 1)  // -> 2, 0
PP_DIV(1, 1), PP_MOD(1, 1)  // -> 1, 0
PP_DIV(0, 1), PP_MOD(0, 1)  // -> 0, 0
PP_DIV(1, 2), PP_MOD(1, 2)  // -> 0, 1
  • PP_DIV_BASE()從三元組(R, X, Y)開始迭代(R初始值為0
  • 迭代操作PP_DIV_BASE_O()返回(R + 1, X - Y, Y)(此處的PP_SUB()內部呼叫PP_WHILE()巨集,構成遞迴重入)
  • 終止條件PP_DIV_BASE_P()X >= Y,此時的R為商、X為餘數(R可能上溢,X不會下溢)

結合模板

有時候,可以使用 C++ 模板 處理型別,不必完全依賴於巨集。例如把函式的class型別引數轉為const T&,而其他型別引數保持T

template <typename T, bool Condition = std::is_class_v<T>>
using maybe_cref_t =
    std::conditional_t<Condition,
                       std::add_lvalue_reference_t<std::add_const_t<T>>,
                       T>;

#define MAKE_ARG(TYPE, IDX, _) \
  PP_COMMA_IF(IDX) maybe_cref_t<TYPE> PP_CONCAT(v, IDX)

// -> void foo(maybe_cref_t<int> v0, maybe_cref_t<std::string> v1);
// -> void foo(int v0, const std::string& v1);
void foo(PP_FOR_EACH(MAKE_ARG, , int, std::string));
  • 巨集 展開結果為maybe_cref_t<int>maybe_cref_t<std::string>
  • C++ 模板 展開結果為intconst std::string&
  • 如果只用巨集,很難完成這項任務

參考資料

本文的用法主要參考以下資料:

寫在最後

本文主要介紹了巨集程式設計的常用方法,但可能存在不足:

  • 不一定適用於所有編譯器(例如 BOOST_PP 原始碼針對 MSVC 做了很多相容處理)
  • 部分程式碼沒考慮到某些特殊場景(例如PP_IS_SYMBOL()不能檢查 以非識別符號開頭的引數)

實際應用場景中,建議使用成熟的預處理庫。

如果有什麼問題,歡迎交流。

Delivered under MIT License © 2020, BOT Man

寫下你的評論...

請問一下,如果想同時宣告一個列舉和對應同名的字串。。用巨集程式設計有沒有比較優雅的方式。。

追加到最後了https://godbolt.org/z/3a8Txc

高質量文章。

我覺得還是 lisp 那種 ast 層面的巨集做 metaprogramming 比較舒坦,c 這個巨集用得太憋屈了。。

根本原因是:C 的巨集常常被 “濫用於” 超程式設計[捂臉][捂臉][捂臉]

Julia 吸收了 Lisp 這一優點。https://docs.julialang.org/en/v1/manual/metaprogramming/

東方巨集模鄉(確信

哪裡都有越共 [捂臉]

巨集超程式設計難得的好文章,總結的很全面。boost 的 pp 庫的使用體驗很糟糕,介面的設計很不正交。好幾年前,我也寫了一個 pp 庫,感覺用起來比 boost 的那個破 pp 庫要方便。用巨集搞圖靈完備,一言難盡,實在是模板超程式設計搞不定,才會動用巨集,又往往能起死回生,真是不可思議。有空我也寫篇文章,說點不一樣的巨集故事

期待一下~~~

奇怪的姿勢增加了

請問一下,如果想同時宣告一個列舉和對應同名的字串。。用巨集程式設計有沒有比較優雅的方式。。

#define ENUM(NAME, ...) enum class NAME { __VA_ARGS__ }; \
std::map<NAME, const char*> PP_CONCAT(NAME, Map) = { PP_FOR_EACH(ENUM_IMPL_EACH, NAME, __VA_ARGS__) }
#define ENUM_IMPL_EACH(VAR, IDX, CTX) PP_COMMA_IF(IDX) { CTX::VAR, PP_STRINGFY(VAR) }

追加到最後了https://godbolt.org/z/3a8Txc

想問一下用巨集的好處是啥(除了程式碼看起來清爽),感覺用了巨集不利於單步除錯,沒辦法看清具體的值

沒有好處,實在沒辦法再用(我的場景是生成一些不需要除錯的程式碼,所以用了巨集)

感謝。巨集如何實現返回多級指標?比如,VOID(N) 表示一個 n 級的 void 指標

可以詳細描述一下需求嗎?[好奇]

c 寫多維陣列記憶體分配,二維陣列返回二級指標 double **, 三維陣列返回 double ***,如果我要申請 1-6 維素組,就得寫 6 個函式,如何寫一個函式接受維數引數,然後可以泛化申請任意維度的陣列的記憶體?可以實現嗎

我來一個釜底抽薪,為什麼不寫一個生成 c++ 程式碼的程式,而要用這麼麻煩的巨集程式設計。另外我覺得 c++ 的模板也是搞的很麻煩。

你說的輪子已經有很多了 [捂臉]

全文完

本文由簡悅 SimpRead優化,用以提升閱讀體驗 使用了全新的簡悅詞法分析引擎beta點選檢視詳細說明

寫在前面如何除錯特殊符號符號拼接自增自減邏輯運算布林轉換條件選擇惰性求值變長引數下標訪問長度判空長度計算遍歷訪問符號匹配資料結構遞迴重入條件迴圈延遲展開數值運算數值比較結合模板參考資料寫在最後