宏定義與可選括號

做者:Mike Ash,原文連接,原文日期:2015-03-20 譯者:俊東;校對:numbbbbbNemocdz;定稿:Pancfhtml

前幾天我遇到了一個有趣的問題:如何編寫一個 C 語言預處理器的宏,刪除包圍實參的括號?git

今天的文章,將爲你們分享個人解決方案。github

起源

C 語言預處理器是一個至關盲目的文本替換引擎,它並不理解 C 代碼,更不用說 Objective-C 了。它的工做原理還算不錯,能夠應付大部分狀況,但偶爾也會出現判斷失誤。swift

這裏舉個典型的例子:數組

objc
XCTAssertEqualObjects(someArray, @[ @"one", @"two" ], @"Array is not as expected");
複製代碼

這會沒法編譯,而且會出現很是古怪的錯誤提示。預處理器查找分隔宏參數的逗號時,沒能將數組結構 @ [...] 中的東西理解爲一個單一的元素。結果代碼嘗試比較 someArray@[@"one"。斷言失敗消息 @"two"]@"Array is not as expected" 是另外的實參。這些半成品部分用於 XCTAssertEqualObjects 的宏擴展中,生成的代碼固然錯得離譜。spa

要解決這個問題也很容易:添加括號就行。預編譯器不能識別 [],但它確實知道 () 而且可以理解應該忽略裏面的逗號。下面的代碼就能正常運行:翻譯

objc
XCTAssertEqualObjects(someArray, (@[ @"one", @"two" ]), @"Array is not as expected");
複製代碼

在 C 語言的許多場景下,你添加多餘的括號也不會有任何區別。宏擴展開以後,生成的代碼雖然在數組文字周圍有括號,但沒有異常。你能夠寫搞笑的多層括號表達式,編譯器會愉快地幫你解析到最裏面一層:code

objc
NSLog(@"%d",((((((((((42)))))))))));
複製代碼

甚至將 NSLog 這樣處理也行:htm

objc
((((((((((NSLog))))))))))(@"%d",42);
複製代碼

在 C 中有一個地方你不能隨意添加括號:類型(types)。例如:blog

objc
int f(void); // 合法
(int) f(void); // 不合法
複製代碼

何時會發生這種狀況呢?這種狀況並不常見,但若是你有一個使用類型的宏,而且類型包含的逗號不在括號內,則會出現這種狀況。宏能夠作不少事情,當一個類型遵循多個協議時,在 Objective-C 中可能出現一些類型帶有未加括號的逗號;當使用帶有多個模板參數的模板化類型時,在 C++ 中也可能出現。舉個例子,這有一個簡單的宏,建立從字典中提供靜態類型值的 getter

objc
#define GETTER(type,name) \
	- (type)name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製代碼

你能這樣使用它:

objc
@implementation SomeClass {
	NSDictionary *_dictionary;
}

GETTER(NSView *,view)
GETTER(NSString *,name)
GETTER(id<NSCopying>,someCopyableThing)
複製代碼

到目前爲止沒問題。如今假設咱們想要建立一個遵循兩個協議的類型:

objc
GETTER(id<NSCopying,NSCoding>,someCopyableAndCodeableThing)
複製代碼

哎呀!宏不起做用了。並且添加括號也無濟於事:

objc
GETTER((id<NSCopying,NSCoding>),someCopyableAndCodeableThing)
複製代碼

這會產生非法代碼。這時咱們須要一個刪除可選括號的 UNPAREN 宏。將 GETTER 宏重寫:

#define GETTER(type,name) \
	- (UNPAREN(type))name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製代碼

咱們該怎麼作呢?

必須的括號

刪除括號很容易:

objc
#define UNPAREN(...) __VA_ARGS__
#define GETTER(type,name) \
	- (UNPAREN type)name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製代碼

雖然看上去很扯,但這的確能運行。預編譯器將 type 擴展爲 (id <NSCopying,NSCoding>),生成 UNPAREN (id<NSCopying, NSCoding>)。而後它會將 UNPAREN 宏擴展爲 id <NSCopying,NSCoding>。括號,消失!

可是,以前使用的 GETTER 失敗了。例如,GETTER(NSView *,view) 在宏擴展中生成 UNPAREN NSView *。不會進一步擴展就直接提供給編譯器。結果天然會報編譯器錯誤,由於 UNPAREN NSView * 是沒法編譯的。這雖然能夠經過編寫 GETTER((NSView *),view) 來解決,可是被迫添加這些括號很煩人。這樣的結果可不是咱們想要的。

宏不能被重載

我馬上想到了如何擺脫剩餘的 UNPAREN。當你想要一個標識符消失時,你可使用一個空的 #define,以下所示:

objc
#define UNPAREN
複製代碼

有了這個,a UNPAREN b 的序列變爲 a b。完美解決問題!可是,若是已經存在帶參數的另外一個定義,則預處理器會拒絕此操做。即便預處理器可能選擇其中一個,它也不會同時存在兩種形式。若是可行的話,這能有效解決咱們的問題,但惋惜的是並不容許:

objc
#define UNPAREN(...) __VA_ARGS__
#define UNPAREN
#define GETTER(type,name) \
	- (UNPAREN type)name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製代碼

這沒法經過預處理器,它會因爲 UNPAREN 的重複 #define 而報錯。不過,它引導咱們走上了成功的道路。如今的瓶頸是怎麼找出一種方法來實現相同的效果,而不會使兩個宏具備相同的名稱。

關鍵

最終目標是讓 UNPAREN(x)UNPAREN((x)) 結果都是 x。朝着這個目標邁出的第一步是製做一些宏,其中傳遞 x(x) 產生相同的輸出,即便它並不肯定 x 是什麼。這能夠經過將宏名稱放在宏擴展中來實現,以下所示:

objc
#define EXTRACT(...) EXTRACT __VA_ARGS__
複製代碼

如今若是你寫 EXTRACT(x),結果是 EXTRACT x。固然,若是你寫 EXTRACT x,結果也是 EXTRACT x,就像沒有宏擴展的狀況。這仍然給咱們留下一個 EXTRACT。雖然不能用 #define 直接解決,但這已經進步了。

標識符粘合

預處理器有一個操做符 ##,它將兩個標識符粘合在一塊兒。例如,a ## b 變爲 ab。這能夠用於從片斷構造標識符,但也能夠用於調用宏。例如:

objc
#define AA 1
#define AB 2
#define A(x) A ## x
複製代碼

從這裏能夠看到,A(A) 產生 1A(B) 產生 2

讓咱們將這個運算符與上面的 EXTRACT 宏結合起來,嘗試生成一個 UNPAREN 宏。因爲 EXTRACT(...) 使用前綴 EXTRACT 生成實參,所以咱們可使用標識符粘合來生成以 EXTRACT 結尾的其餘標記。若是咱們 #define 那個新標記爲空,那就搞定了。

這是一個以 EXTRACT 結尾的宏,它不會產生任何結果:

objc
#define NOTHING_EXTRACT
複製代碼

這是對 UNPAREN 宏的嘗試,它將全部內容放在一塊兒:

objc
#define UNPAREN(x) NOTHING_ ## EXTRACT x
複製代碼

不幸的是,這並不能實現咱們的目標。問題在操做順序上。若是咱們寫 UNPAREN((int)),咱們將會獲得:

objc
UNPAREN((int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)
複製代碼

標示符粘合太早起做用,EXTRACT 宏永遠不會有機會擴展開。

可使用間接的方式強制預處理器用不一樣的順序判斷事件。咱們能夠製做一個 PASTE 宏,而不是直接使用 ##

objc
#define PASTE(x,...) x ## __VA_ARGS__
複製代碼

而後咱們將根據它編寫 UNPAREN

objc
#define UNPAREN(x)  PASTE(NOTHING_,EXTRACT x)
複製代碼

仍然不起做用。狀況以下:

objc
UNPAREN((int))
PASTE(NOTHING_,EXTRACT (int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)
複製代碼

但更接近咱們的目標了。序列 EXTRACT(int) 顯然沒有觸發標示符粘合操做符。咱們必須讓預處理器在它看到 ## 以前解析它。能夠經過另外一種方式間接強制解析它。讓咱們定義一個只包裝 PASTEEVALUATING_PASTE 宏:

objc
#define EVALUATING_PASTE(x,...) PASTE(x,__VA_ARGS__)
複製代碼

如今讓咱們用UNPAREN

objc
#define UNPAREN(x) EVALUATING_PASTE(NOTHING_,EXTRACT x)
複製代碼

這是展開以後:

objc    
UNPAREN((int))
EVALUATING_PASTE(NOTHING_,EXTRACT (int))
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int
複製代碼

即便沒有額外加括號也能正常運行,由於額外的賦值並無影響:

objc
UNPAREN(int)
EVALUATING_PASTE(NOTHING_,EXTRACT int)
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int
複製代碼

成功了!咱們如今編寫 GETTER 時能夠不須要圍繞類型的括號了:

objc
#define GETTER(type,name) \
	- (UNPAREN(type))name { \
		return [_dictionary objectForKey: @#name]; \
	}
複製代碼

獎勵宏

在選擇一些宏來證實這個結構時,我構建了一個很好的 dispatch_once 宏來製做延遲初始化的常量。實現以下:

objc
#define ONCE(type,name,...) \
	UNPAREN(type) name() { \
		static UNPAREN(type) static_ ## name; \
		static dispatch_once_t predicate; \
		dispatch_once(&predicate,^{ \
			static_ ## name = ({ __VA_ARGS__; }); \
		}); \
		return static_ ## name; \
	}
複製代碼

使用案例:

objc
ONCE(NSSet *,AllowedFileTypes,[NSSet setWithArray:@[ @"mp3",@"m4a",@"aiff" ]])
複製代碼

而後,你能夠調用 AllowedFileTypes() 來獲取集合,並根據須要高效建立集合。若是類型不巧包括括號,添加括號就能運行。

結論

僅僅寫這個宏,我就發現了不少艱澀的知識。我但願接觸這些知識也不會影響你的思惟。請謹慎使用這些知識。

今天就這樣。之後還會有更多使人興奮的探索,可能比這還要再難以想象。在此以前,若是你對此主題有任何建議,請發送給 咱們

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg

相關文章
相關標籤/搜索