C語言遞歸宏的具體使用
一、先把地基壓實:C 宏并不會“真遞歸”
C 標(biāo)準(zhǔn)(以 C17 為例)規(guī)定了宏展開的單次掃描與不立即再次展開相同標(biāo)識符的規(guī)則。簡化地說,這段代碼不會像函數(shù)那樣在運行期一層層壓棧,而是在編譯前由預(yù)處理器做一次(或多次)符號替換。
#define SELF(X) SELF(X) // 期待“遞歸”?不會成功 int x = SELF(1); // 預(yù)處理器直接拒絕/死循環(huán)保護
要點:所謂“遞歸宏”,實際上是通過構(gòu)造“延遲展開(defer)”和“多輪掃描(rescan)”,讓預(yù)處理器在后續(xù)輪次再去展開目標(biāo),從而模擬“遞歸”的效果。
二、故事線索:一份命令表,三處復(fù)用
項目里有一張“命令定義清單”。經(jīng)典解法是 X-Macro:把數(shù)據(jù)集中放在一個表里,用不同的宏姿勢多次“播放”。
// commands.def —— 只維護這一份 // 格式: X(標(biāo)識符, "字符串", 編碼) X(CMD_FOO, "foo", 0x01) X(CMD_BAR, "bar", 0x02) X(CMD_BAZ, "baz", 0x03)
然后在不同位置這樣用:
// 1) 枚舉
#define X(name, str, code) name = code,
enum Command {
#include "commands.def"
};
#undef X
// 2) 字符串?dāng)?shù)組
#define X(name, str, code) [code] = str,
const char* kCmdName[0x100] = {
#include "commands.def"
};
#undef X
這已經(jīng)解決了 “一處維護、多處生成” 的大半問題。可一旦想“遍歷變長參數(shù)”或“編出嵌套結(jié)構(gòu)”,就會撞到 X-Macro 的邊界,于是“遞歸宏”的戲份來了。
三、遞歸宏的核心技巧:延遲與阻斷
想象成一句“別現(xiàn)在展開,等下一輪再說”。常見做法是準(zhǔn)備三個工具宏:
#define EMPTY() #define DEFER(id) id EMPTY() // 把 id 的展開推遲一輪 #define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)() // 阻斷+再延期 #define EXPAND(...) __VA_ARGS__ // 有時配合編譯器做再展開
它們本身不神秘,重點在讓預(yù)處理器別一次性把所有層級吃完。這樣才能模擬“你先記著,下一口再嚼”的節(jié)奏。
例子1:給可變參數(shù)“逐個套殼”(MAP)
目標(biāo):把 MAP(WRAP, A, B, C) 變成 WRAP(A) WRAP(B) WRAP(C)。一份最小可用(且便于理解)的實現(xiàn)如下:
// 判空與“還有參數(shù)嗎”的探針(簡化版)
#define EVAL(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) __VA_ARGS__
#define CAT(a,b) a##b
#define PROBE() ~, 1
#define IS_PROBE(...) CAT(IS_PROBE_, __VA_ARGS__)
#define IS_PROBE_~ , 0
#define SECOND(a, b, ...) b
#define NOT(x) IS_PROBE(CAT(PROBE_ , x))
#define BOOL(x) NOT(NOT(x))
#define IF(c) CAT(IF_, c)
#define IF_1(t, ...) t
#define IF_0(t, ...) __VA_ARGS__
// 取第一個參數(shù)與余下參數(shù)
#define HEAD(x, ...) x
#define TAIL(x, ...) __VA_ARGS__
// 檢測是否還有參數(shù)(極簡近似)
#define HAS_ARGS(...) BOOL(SECOND(__VA_ARGS__ , ~))
// 遞歸樣式的 MAP
#define MAP(f, first, ...) \
f(first) \
IF(HAS_ARGS(__VA_ARGS__))( \
OBSTRUCT()(DEFER(MAP)()(f, __VA_ARGS__)) \
, /* empty */)
#define WRAP(x) [x]
/* 使用 */
EVAL(MAP(WRAP, A, B, C)) // 結(jié)果: [A] [B] [C]
這里的關(guān)鍵就是 OBSTRUCT 和 DEFER:當(dāng) MAP 需要“再處理余下參數(shù)”時,它不立刻調(diào)用自己,而是故意推遲一個展開節(jié)拍,交給下一輪掃描,于是在視覺上形成了“遞歸”。
例子2:把鍵值對轉(zhuǎn)成結(jié)構(gòu)初始化
當(dāng)命令表變復(fù)雜,可能出現(xiàn)嵌套:
// pairs.def P(kFoo, 1) P(kBar, 2) P(kBaz, 3)
想要生成:
struct Item { const char* name; int code; };
struct Item items[] = {
{"kFoo", 1}, {"kBar", 2}, {"kBaz", 3}
};
可以在 P 前后套“遞歸 MAP”:
#define AS_ITEM(pair_name, code) { #pair_name, code },
#define LIST(...) EVAL(MAP(AS_ITEM, __VA_ARGS__))
struct Item items[] = {
LIST(
// 讓 def 文件只管內(nèi)容,外面用“展開器”來遍歷
#define P(name, code) name, code
#include "pairs.def"
#undef P
)
};
四、工程可落地的三板斧
1)把“算法”封到獨立頭文件:例如 pp_map.h 專管 MAP/CHAIN/REPEAT 等。業(yè)務(wù)側(cè)只 include,不反復(fù)復(fù)制宏魔法。
2)給每條“魔法”寫一次“快照單測”:對編譯后產(chǎn)物用 static_assert 或數(shù)組大小校驗,確保未來改動不把生成結(jié)果弄壞。
3)設(shè)置最大深度:遞歸宏最終還是有限步“吃完”。為 MAP 等提供“最大次數(shù)保險絲”,避免極端輸入卡在預(yù)處理階段。
// 例如硬性限制最多處理 64 項 #define LIMIT_64(...) __VA_ARGS__ #define MAP_LIMITED(f, ...) LIMIT_64(EVAL(MAP(f, __VA_ARGS__)))
五、與現(xiàn)成庫牽手:Boost.Preprocessor 的啟發(fā)
C++ 項目常用的 Boost.Preprocessor 在 C 代碼里也能借鑒思想(宏只是預(yù)處理期產(chǎn)物)。它提供 BOOST_PP_REPEAT、BOOST_PP_SEQ_FOR_EACH 等成熟原語,足以覆蓋多數(shù)“列表掃描”“計數(shù)”“拼接”需求。
Boost.Preprocessor 官網(wǎng):https://www.boost.org/doc/libs/release/libs/preprocessor/
六、再回到故事:三處生成,一處真相
命令清單只維護在一處;
通過 X-Macro 完成“結(jié)構(gòu)性多次播放”;
在需要“變長/嵌套遍歷”的地方,用**遞歸宏(延遲展開)**把“列表加工”補齊。
落地后,增刪命令就只改一行,枚舉、字符串、解析器保持同步。
七、邊界與避坑清單(工程經(jīng)驗濃縮)
- 可讀性:宏層層交錯非?;逎?。務(wù)必寫注釋,尤其解釋
DEFER/OBSTRUCT的意圖與“多輪掃描”的必要性。 - 編譯器差異:不同編譯器對“再次掃描”的策略略有差別;遇到怪異行為,優(yōu)先用
EVAL/EVAL1/EVAL2...逼出足夠的展開輪次。 - 調(diào)試手段:
-E導(dǎo)出預(yù)處理結(jié)果;或在 IDE 里開啟“預(yù)處理產(chǎn)物”查看。 - 邊做邊收斂:一旦邏輯成長為“編程語言中的編程語言”,要審視是否引入 Python/生成腳本更合適。
九、再給兩個“像真遞歸”的小玩具
計數(shù):編譯期統(tǒng)計參數(shù)個數(shù)
#define ARG_N( \ _1,_2,_3,_4,_5,_6,_7,_8,_9,_10, \ _11,_12,_13,_14,_15,_16,_17,_18,_19,_20, \ N, ...) N #define RSEQ() \ 20,19,18,17,16,15,14,13,12,11,10, \ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 #define NARGS_(...) ARG_N(__VA_ARGS__) #define NARGS(...) NARGS_(__VA_ARGS__, RSEQ()) // 用法 static int a[NARGS(A,B,C) == 3 ? 1 : -1]; // 通過即為 3
在宏里“折疊”一棵小表達(dá)式樹
#define PLUS(a,b) ((a)+(b))
#define TIMES(a,b) ((a)*(b))
// FOLD(PLUS, 0, 1,2,3) => (((0+1)+2)+3)
#define FOLD(op, init, first, ...) \
op(init, first) \
IF(HAS_ARGS(__VA_ARGS__))( \
OBSTRUCT()(DEFER(FOLD)()(op, /* init= */ op(init, first), __VA_ARGS__)) \
, /* empty */)
到此這篇關(guān)于C語言遞歸宏的具體使用的文章就介紹到這了,更多相關(guān)C語言 遞歸宏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++結(jié)構(gòu)體struct和類class區(qū)別詳解
struct和class有什么區(qū)別?最本質(zhì)的一個區(qū)別就是默認(rèn)的訪問控制:默認(rèn)的繼承訪問權(quán)限,struct是public的,class是private的。2017-11-11
詳解C++編程中的條件判斷語句if-else與switch的用法
這篇文章主要介紹了C++編程中的條件判斷語句if-else與switch的用法,是C++入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2016-01-01
使用C++11實現(xiàn)Android系統(tǒng)的Handler機制
這篇文章主要介紹了使用C++11實現(xiàn)Android系統(tǒng)的Handler機制,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04

