宏定义与可选括号

做者: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

相关文章
相关标签/搜索