C++ 预处理与宏相关编程(#,##等等)
一、簡(jiǎn)介引言
C++?模板元編程?(template metaprogramming)?雖然功能強(qiáng)大,但也有?局限性:
- 不能通過(guò) 模板展開(kāi) 生成新的?標(biāo)識(shí)符(identifier)。例如:生成新的 函數(shù)名、類名、名字空間名 等
- 使用者 只能使用 預(yù)先定義的標(biāo)識(shí)符,不能通過(guò) 模板參數(shù) 獲取?符號(hào)/標(biāo)記(token)?的?字面量(literal)
- 例如 在反射中獲取 實(shí)參參數(shù)名的字面量,在斷言中獲取 表達(dá)式的字面量。
所以,在需要直接?操作標(biāo)識(shí)符?的情況下,還需要借助?宏,進(jìn)行?預(yù)處理階段的元編程:
- 和?編譯時(shí)(compile-time)?的?模板?展開(kāi)不同,宏?在編譯前的預(yù)處理(preprocess)?階段全部展開(kāi) —— 狹義上,編譯器 看不到且不處理 宏代碼。
- 通過(guò)?#define/TOKEN1##TOKEN2/#TOKEN?定義?宏對(duì)象(object-like macro)?和?宏函數(shù)(function-like macro),可以實(shí)現(xiàn)替換文本、拼接標(biāo)識(shí)符、獲取字面量等功能。
?
1.1? 關(guān)于C++宏編程調(diào)試
很多人因?yàn)?“宏編程” 無(wú)法調(diào)試,而直接 “從入門到放棄” —— 不經(jīng)意的?符號(hào)拼寫錯(cuò)誤、參數(shù)個(gè)數(shù)錯(cuò)誤,導(dǎo)致文本?不能正確替換,從而帶來(lái)?滿屏的編譯錯(cuò)誤,最后?難以定位?問(wèn)題所在 ——
- 最壞的情況下,編譯器?只會(huì)告訴你?cpp 文件 編譯時(shí)出現(xiàn)?語(yǔ)法錯(cuò)誤
- 最好的情況下,編譯器?可能告訴你?XXX 宏 展開(kāi)結(jié)果里包含?語(yǔ)法錯(cuò)誤
- 而永遠(yuǎn)?不會(huì)告訴你?是因?yàn)?XXX 宏展開(kāi)成什么樣,導(dǎo)致 YYY 宏展開(kāi)失敗
- 最后?只能看到?ZZZ 宏展開(kāi)錯(cuò)誤
由于宏代碼會(huì) 在編譯前全部展開(kāi),我們可以:
- 讓編譯器?僅輸出預(yù)處理結(jié)果
- gcc -E?讓編譯器 在預(yù)處理結(jié)束后停止,不進(jìn)行 編譯、鏈接
- gcc -P?屏蔽編譯器 輸出預(yù)處理結(jié)果的?行標(biāo)記 (linemarker),減少干擾。并結(jié)合??__LINE__;;; 宏實(shí)現(xiàn)代碼行數(shù)和位置的定位。
- 另外,由于輸出結(jié)果沒(méi)有格式化,建議先傳給?clang-format?格式化后再輸出
- 屏蔽?無(wú)關(guān)的?頭文件
?
二、宏編程的常見(jiàn)使用模式
C(和C++)中的宏(Macro)屬于編譯器預(yù)處理的范疇,屬于編譯期概念(而非運(yùn)行期概念)。下面對(duì)常遇到的宏的使用問(wèn)題做了簡(jiǎn)單總結(jié)。
2.1 關(guān)于#和## (# 符號(hào)拼接)
在C語(yǔ)言的宏中,#的功能是將其后面的宏參數(shù)進(jìn)行字符串化操作(Stringfication),簡(jiǎn)單說(shuō)就是在對(duì)它所引用的宏變量通過(guò)替換后在其左右各加上一個(gè)雙引號(hào)。比如下面代碼中的宏:
#define WARN_IF(EXP) \ do { \if (EXP) \ fprintf(stderr, "Warning: " #EXP "\n"); \ }while(0)那么實(shí)際使用中會(huì)出現(xiàn)下面所示的替換過(guò)程:
WARN_IF (divider == 0);
被替換為:
do { if (divider == 0) fprintf(stderr, "Warning" "divider == 0" "\n"); } while(0);這樣每次divider(除數(shù))為0的時(shí)候便會(huì)在標(biāo)準(zhǔn)錯(cuò)誤流上輸出一個(gè)提示信息。
而## 被稱為連接符(concatenator),用來(lái)將兩個(gè)Token連接為一個(gè)Token。注意這里連接的對(duì)象是Token就行,而不一定是宏的變量。比如你要做一個(gè)菜單項(xiàng)命令名和函數(shù)指針組成的結(jié)構(gòu)體的數(shù)組,并且希望在函數(shù)名和菜單項(xiàng)命令名之間有直觀的,名字上的關(guān)系。那么下面的代碼就非常實(shí)用:
struct command { char * name; void (*function) (void); }; #define COMMAND(NAME) { NAME, NAME ## _command }然后你就用一些預(yù)先定義好的命令來(lái)方便的初始化一個(gè)command結(jié)構(gòu)的數(shù)組了:
struct command commands[] = { COMMAND(quit), COMMAND(help), ... }COMMAND宏在這里充當(dāng)一個(gè)代碼生成器的作用,這樣可以在一定程度上減少代碼密度,間接地也可以減少不留心所造成的錯(cuò)誤。我們還可以n個(gè)##符號(hào)連接 n+1個(gè)Token,這個(gè)特性也是#符號(hào)所不具備的。比如:
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d typedef struct _record_type LINK_MULTIPLE(name,company,position,salary);這里這個(gè)語(yǔ)句將展開(kāi)為:
typedef struct _record_type name_company_position_salary;2.2 關(guān)于...的使用(## 變長(zhǎng)參數(shù))
...在C宏中稱為Variadic Macro,也就是變參宏。
在GNU C中,從C99開(kāi)始,宏可以接受可變數(shù)目的參數(shù),就象可變參數(shù)函數(shù)一樣。和函數(shù)一樣,宏也用三個(gè)點(diǎn)…來(lái)表示可變參數(shù)
__VA_ARGS__ 宏
__VA_ARGS__ 宏用來(lái)表示可變參數(shù)的內(nèi)容,簡(jiǎn)單來(lái)說(shuō)就是將左邊宏中 … 的內(nèi)容原樣抄寫在右邊__VA_ARGS__ 所在的位置。如下例代碼:
#include <stdio.h> #define debug(...) printf(__VA_ARGS__) int main(void) {int year = 2018;debug("this year is %d\n", year); //效果同printf("this year is %d\n", year); }可變參數(shù)別稱
另外,通過(guò)一些語(yǔ)法,你可以給可變參數(shù)起一個(gè)名字,而不是使用__VA_ARGS__ ,如下例中的args:
#include <stdio.h> #define debug(format, args...) printf(format, args) int main(void) {int year = 2018;debug("this year is %d\n", year); //效果同printf("this year is %d\n", year); }無(wú)參傳入情況
與可變參數(shù)函數(shù)不同的是,可變參數(shù)宏中的可變參數(shù)必須至少有一個(gè)參數(shù)傳入,不然會(huì)報(bào)錯(cuò),為了解決這個(gè)問(wèn)題,需要一個(gè)特殊的“##”操作,如果可變參數(shù)被忽略或?yàn)榭?#xff0c;“##”操作將使預(yù)處理器(preprocessor)去除掉它前面的那個(gè)逗號(hào)。如下例所示
#include <stdio.h> #define debug(format, args...) printf(format, ##args) int main(void) {int year = 2018;debug("hello, world"); //只有format參數(shù),沒(méi)有args可變參數(shù) }- 宏連接符##
舉個(gè)例子:宏定義為#define XNAME(n) x##n,代碼為:XNAME(4),則在預(yù)編譯時(shí),宏發(fā)現(xiàn)XNAME(4)與XNAME(n)匹配,則令 n 為 4,然后將右邊的n的內(nèi)容也變?yōu)?,然后將整個(gè)XNAME(4)替換為 x##n,亦即 x4,故最終結(jié)果為 XNAME(4) 變?yōu)?x4。如下例所示:
#include <stdio.h> #define XNAME(n) x##n #define PRINT_XN(n) printf("x" #n " = %d\n", x##n); int main(void) {int XNAME(1) = 14; // becomes int x1 = 14;int XNAME(2) = 20; // becomes int x2 = 20;PRINT_XN(1); // becomes printf("x1 = %d\n", x1);PRINT_XN(2); // becomes printf("x2 = %d\n", x2);return 0; }2.3? 特殊符號(hào)
和模板元編程不一樣,宏編程?沒(méi)有類型?的概念,輸入和輸出都是?符號(hào)?—— 不涉及編譯時(shí)的 C++ 語(yǔ)法,只進(jìn)行編譯前的?文本替換:
- 一個(gè)?宏參數(shù)?是一個(gè)任意的?符號(hào)序列(token sequence),不同宏參數(shù)之間 用逗號(hào)分隔
- 每個(gè)參數(shù)可以是?空序列,且空白字符會(huì)被忽略(例如?a + 1?和?a+1?相同)
- 在一個(gè)參數(shù)內(nèi),不能出現(xiàn)?逗號(hào)(comma)?或 不配對(duì)的?括號(hào)(parenthesis)(例如?FOO(bool, std::pair<int, int>)?被認(rèn)為是?FOO()?有三個(gè)參數(shù):bool?/?std::pair<int?/?int>)
如果需要把?std::pair<int, int>?作為一個(gè)參數(shù),一種方法是使用 C++ 的?類型別名?(type alias)(例如?using IntPair = std::pair<int, int>;),避免 參數(shù)中出現(xiàn)逗號(hào)(即?FOO(bool, IntPair)?只有兩個(gè)參數(shù))。
更通用的方法是使用?括號(hào)對(duì)?封裝每個(gè)參數(shù)(下文稱為?元組),并在最終展開(kāi)時(shí) 移除括號(hào)(元組解包)即可:
#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)?展開(kāi)為?PP_REMOVE_PARENS_IMPL T?的形式
- 如果參數(shù)?T?是一個(gè)?括號(hào)對(duì),那么展開(kāi)結(jié)果會(huì)變成?調(diào)用宏函數(shù)PP_REMOVE_PARENS_IMPL (...)?的形式
- 接著,PP_REMOVE_PARENS_IMPL(...)?再展開(kāi)為參數(shù)本身?__VA_ARGS__(下文提到的?變長(zhǎng)參數(shù)),即元組?T?的內(nèi)容
另外,常用宏函數(shù) 代替?特殊符號(hào),用于下文提到的?惰性求值:
#define PP_COMMA() , #define PP_LPAREN() ( #define PP_RPAREN() ) #define PP_EMPTY()2.4 其他
宏編程也具有強(qiáng)大的計(jì)算能力,是否是圖靈完備型。筆者尚不確定。不過(guò)如?BOOST_PP:目前流行的?預(yù)處理庫(kù)(preprocessor library) 里利用宏編程實(shí)現(xiàn)了很多計(jì)算語(yǔ)句的基礎(chǔ)結(jié)構(gòu),如:自增自減、邏輯運(yùn)算、布爾轉(zhuǎn)換、條件選擇、惰性求值、下標(biāo)訪問(wèn)、參數(shù)長(zhǎng)度判空,長(zhǎng)度計(jì)算、遍歷訪問(wèn)、符號(hào)匹配、以及提供了常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)(如 元組、序列、列表、數(shù)組等)、此外還有數(shù)值運(yùn)算、數(shù)值比較等等。可以參見(jiàn)知乎一篇文章:C/C++ 宏編程的藝術(shù)。
?
三、使用宏時(shí)需要注意的點(diǎn)
3.1? 錯(cuò)誤的嵌套-Misnesting
宏的定義不一定要有完整的、配對(duì)的括號(hào),但是為了避免出錯(cuò)并且提高可讀性,最好避免這樣使用。
3.2? 由操作符優(yōu)先級(jí)引起的問(wèn)題-Operator Precedence Problem
由于宏只是簡(jiǎn)單的替換,宏的參數(shù)如果是復(fù)合結(jié)構(gòu),那么通過(guò)替換之后可能由于各個(gè)參數(shù)之間的操作符優(yōu)先級(jí)高于單個(gè)參數(shù)內(nèi)部各部分之間相互作用的操作符優(yōu)先級(jí),如果我們不用括號(hào)保護(hù)各個(gè)宏參數(shù),可能會(huì)產(chǎn)生預(yù)想不到的情形。比如:
#define ceil_div(x, y) (x + y - 1) / y那么
a = ceil_div( b & c, sizeof(int) );將被轉(zhuǎn)化為:
a = ( b & c + sizeof(int) - 1) / sizeof(int);由于+/-的優(yōu)先級(jí)高于&的優(yōu)先級(jí),那么上面式子等同于:
a = ( b & (c + sizeof(int) - 1)) / sizeof(int);這顯然不是調(diào)用者的初衷。為了避免這種情況發(fā)生,應(yīng)當(dāng)多寫幾個(gè)括號(hào):
#define ceil_div(x, y) (((x) + (y) - 1) / (y))3.3?消除多余的分號(hào)-Semicolon Swallowing
通常情況下,為了使函數(shù)模樣的宏在表面上看起來(lái)像一個(gè)通常的C語(yǔ)言調(diào)用一樣,通常情況下我們?cè)诤甑暮竺婕由弦粋€(gè)分號(hào),比如下面的帶參宏:
MY_MACRO(x);但是如果是下面的情況:
#define MY_MACRO(x) { \ /* line 1 */ \ /* line 2 */ \ /* line 3 */ } //... if (condition()) MY_MACRO(a); else {... }這樣會(huì)由于多出的那個(gè)分號(hào)產(chǎn)生編譯錯(cuò)誤。為了避免這種情況出現(xiàn)同時(shí)保持MY_MACRO(x);的這種寫法,我們需要把宏定義為這種形式:
#define MY_MACRO(x) do { /* line 1 */ \ /* line 2 */ \ /* line 3 */ } while(0)這樣只要保證總是使用分號(hào),就不會(huì)有任何問(wèn)題。
3.4 Duplication of Side Effects
這里的Side Effect是指宏在展開(kāi)的時(shí)候?qū)ζ鋮?shù)可能進(jìn)行多次Evaluation(也就是取值),但是如果這個(gè)宏參數(shù)是一個(gè)函數(shù),那么就有可能被調(diào)用多次從而達(dá)到不一致的結(jié)果,甚至?xí)l(fā)生更嚴(yán)重的錯(cuò)誤。比如:
#define min(X,Y) ((X) > (Y) ? (Y) : (X)) //... c = min(a,foo(b));這時(shí)foo()函數(shù)就被調(diào)用了兩次。為了解決這個(gè)潛在的問(wèn)題,我們應(yīng)當(dāng)這樣寫min(X,Y)這個(gè)宏:
#define min(X,Y) ({ \ typeof (X) x_ = (X); \ typeof (Y) y_ = (Y); \ (x_ < y_) ? x_ : y_; })({...})的作用是將內(nèi)部的幾條語(yǔ)句中最后一條的值返回,它也允許在內(nèi)部聲明變量(因?yàn)樗ㄟ^(guò)大括號(hào)組成了一個(gè)局部Scope)。
一些有意思的問(wèn)題
- 下面的代碼:
運(yùn)行結(jié)果是name,為什么不是"#name"呢?
#在這里是字符串化的意思,printf(""#name"") 相當(dāng)于 printf("" "name" "").
printf("" #name "") <1>
相當(dāng)于printf("" "name" "") <2>
而<2>中的第2,3個(gè)“中間時(shí)空格 等價(jià)于("空+name+空')
## 連接符號(hào)由兩個(gè)井號(hào)組成,其功能是在帶參數(shù)的宏定義中將兩個(gè)子串(token)聯(lián)接起來(lái),從而形成一個(gè)新的子串。但它不可以是第一個(gè)或者最后一個(gè)子串。所謂的子串 (token)就是指編譯器能夠識(shí)別的最小語(yǔ)法單元。具體的定義在編譯原理里有詳盡的解釋,但不知道也無(wú)所謂。同時(shí)值得注意的是#符是把傳遞過(guò)來(lái)的參數(shù)當(dāng)成字符串進(jìn)行替代。下面來(lái)看看它們是怎樣工作的。這是MSDN上的一個(gè)例子。
假設(shè)程序中已經(jīng)定義了這樣一個(gè)帶參數(shù)的宏
#define paster( n ) printf( "token" #n " = %d", token##n )同時(shí)又定義了一個(gè)整形變量:
int token9 = 9;現(xiàn)在在主程序中以下面的方式調(diào)用這個(gè)宏:
paster(9);那么在編譯時(shí),上面的這句話被擴(kuò)展為:
printf( "token" "9" " = %d", token9 );注意到在這個(gè)例子中,paster(9);中的這個(gè)”9”被原封不動(dòng)的當(dāng)成了一個(gè)字符串,與”token”連接在了一起,從而成為了token9。而#n也被”9”所替代。
可想而知,上面程序運(yùn)行的結(jié)果就是在屏幕上打印出token9=9.
#define display(name) printf(""#name"") int main() { display(name); }特殊性就在于它是個(gè)宏,宏里面處理#號(hào)就如LS所說(shuō)!
處理后就是一個(gè)附加的字符串!
但printf(""#name"");就不行了!
#define display(name) printf(""#name"")該定義字符串化name,得到結(jié)果其實(shí)就是printf("name")(前后的空字符串拿掉), 這樣輸出來(lái)的自然是 name .
從另外一個(gè)角度講, #是一個(gè)連接符號(hào), 參與運(yùn)算了, 自然不會(huì)輸出了.
?
?
總結(jié)
以上是生活随笔為你收集整理的C++ 预处理与宏相关编程(#,##等等)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【数据结构与算法】AVL树核心算法的Ja
- 下一篇: 大量Input还是要靠scanf(洛谷P