[翻译][SwiftGG]宏定义与可选括号

原文链接

SwiftGG链接

前几天我遇到了一个有趣的问题:如何编写一个 C 语言预处理器的宏,删除包围实参的括号?

今天的文章,将为大家分享我的解决方案。

起源

C 语言预处理器是一个相当盲目的文本替换引擎,它并不理解 C 代码,更不用说 Objective-C 了。它的工作原理还算不错,可以应付大部分情况,但偶尔也会出现判断失误。

这里举个典型的例子:

1
XCTAssertEqualObjects(someArray, @[ @"one", @"two" ], @"Array is not as expected");

这会无法编译,并且会出现非常古怪的错误提示。预处理器查找分隔宏参数的逗号时,没能将数组结构 @ [...] 中的东西理解为一个单一的元素。结果代码尝试比较 someArray@[@"one"。断言失败消息 @"two"]@"Array is not as expected" 是另外的实参。这些半成品部分用于 XCTAssertEqualObjects 的宏扩展中,生成的代码当然错得离谱。

要解决这个问题也很容易:添加括号就行。预编译器不能识别 [],但它确实知道 () 并且能够理解应该忽略里面的逗号。下面的代码就能正常运行:

1
XCTAssertEqualObjects(someArray, (@[ @"one", @"two" ]), @"Array is not as expected");

在 C 语言的许多场景下,你添加多余的括号也不会有任何区别。宏扩展开之后,生成的代码虽然在数组文字周围有括号,但没有异常。你可以写搞笑的多层括号表达式,编译器会愉快地帮你解析到最里面一层:

1
NSLog(@"%d",((((((((((42)))))))))));

甚至将 NSLog 这样处理也行:

1
((((((((((NSLog))))))))))(@"%d",42);

在 C 中有一个地方你不能随意添加括号:类型(types)。例如:

1
2
int f(void); // 合法
(int) f(void); // 不合法

什么时候会发生这种情况呢?这种情况并不常见,但如果你有一个使用类型的宏,并且类型包含的逗号不在括号内,则会出现这种情况。宏可以做很多事情,当一个类型遵循多个协议时,在 Objective-C 中可能出现一些类型带有未加括号的逗号;当使用带有多个模板参数的模板化类型时,在 C++ 中也可能出现。举个例子,这有一个简单的宏,创建从字典中提供静态类型值的 getter

1
2
3
4
#define GETTER(type,name) \
- (type)name { \
return [_dictionary objectForKey: @#name]; \
}

你能这样使用它:

1
2
3
4
5
6
7
@implementation SomeClass {
NSDictionary *_dictionary;
}

GETTER(NSView *,view)
GETTER(NSString *,name)
GETTER(id<NSCopying>,someCopyableThing)

到目前为止没问题。现在假设我们想要创建一个遵循两个协议的类型:

1
GETTER(id<NSCopying,NSCoding>,someCopyableAndCodeableThing)

哎呀!宏不起作用了。而且添加括号也无济于事:

1
GETTER((id<NSCopying,NSCoding>),someCopyableAndCodeableThing)

这会产生非法代码。这时我们需要一个删除可选括号的 UNPAREN 宏。将 GETTER 宏重写:

1
2
3
4
#define GETTER(type,name) \
- (UNPAREN(type))name { \
return [_dictionary objectForKey: @#name]; \
}

我们该怎么做呢?

必须的括号

删除括号很容易:

1
2
3
4
5
#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,如下所示:

1
#define UNPAREN

有了这个,a UNPAREN b 的序列变为 a b。完美解决问题!但是,如果已经存在带参数的另一个定义,则预处理器会拒绝此操作。即使预处理器可能选择其中一个,它也不会同时存在两种形式。如果可行的话,这能有效解决我们的问题,但可惜的是并不允许:

1
2
3
4
5
6
#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 是什么。这可以通过将宏名称放在宏扩展中来实现,如下所示:

1
#define EXTRACT(...) EXTRACT __VA_ARGS__

现在如果你写 EXTRACT(x),结果是 EXTRACT x。当然,如果你写 EXTRACT x,结果也是 EXTRACT x,就像没有宏扩展的情况。这仍然给我们留下一个 EXTRACT。虽然不能用 #define 直接解决,但这已经进步了。

标识符粘合

预处理器有一个操作符 ##,它将两个标识符粘合在一起。例如,a ## b 变为 ab。这可以用于从片段构造标识符,但也可以用于调用宏。例如:

1
2
3
#define AA 1
#define AB 2
#define A(x) A ## x

从这里可以看到,A(A) 产生 1A(B) 产生 2

让我们将这个运算符与上面的 EXTRACT 宏结合起来,尝试生成一个 UNPAREN 宏。由于 EXTRACT(...) 使用前缀 EXTRACT 生成实参,因此我们可以使用标识符粘合来生成以 EXTRACT 结尾的其他标记。如果我们 #define 那个新标记为空,那就搞定了。

这是一个以 EXTRACT 结尾的宏,它不会产生任何结果:

1
#define NOTHING_EXTRACT

这是对 UNPAREN 宏的尝试,它将所有内容放在一起:

1
#define UNPAREN(x) NOTHING_ ## EXTRACT x

不幸的是,这并不能实现我们的目标。问题在操作顺序上。如果我们写 UNPAREN((int)),我们将会得到:

1
2
3
4
UNPAREN((int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)

标示符粘合太早起作用,EXTRACT 宏永远不会有机会扩展开。

可以使用间接的方式强制预处理器用不同的顺序判断事件。我们可以制作一个 PASTE 宏,而不是直接使用 ##

1
#define PASTE(x,...) x ## __VA_ARGS__

然后我们将根据它编写 UNPAREN

1
#define UNPAREN(x)  PASTE(NOTHING_,EXTRACT x)

仍然不起作用。情况如下:

1
2
3
4
5
UNPAREN((int))
PASTE(NOTHING_,EXTRACT (int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)

但更接近我们的目标了。序列 EXTRACT(int) 显然没有触发标示符粘合操作符。我们必须让预处理器在它看到 ## 之前解析它。可以通过另一种方式间接强制解析它。让我们定义一个只包装 PASTEEVALUATING_PASTE 宏:

1
#define EVALUATING_PASTE(x,...) PASTE(x,__VA_ARGS__)

现在让我们用UNPAREN

1
#define UNPAREN(x) EVALUATING_PASTE(NOTHING_,EXTRACT x)

这是展开之后:

1
2
3
4
5
6
UNPAREN((int))
EVALUATING_PASTE(NOTHING_,EXTRACT (int))
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int

即使没有额外加括号也能正常运行,因为额外的赋值并没有影响:

1
2
3
4
5
6
UNPAREN(int)
EVALUATING_PASTE(NOTHING_,EXTRACT int)
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int

成功了!我们现在编写 GETTER 时可以不需要围绕类型的括号了:

1
2
3
4
#define GETTER(type,name) \
- (UNPAREN(type))name { \
return [_dictionary objectForKey: @#name]; \
}

奖励宏

在选择一些宏来证明这个结构时,我构建了一个很好的 dispatch_once 宏来制作延迟初始化的常量。实现如下:

1
2
3
4
5
6
7
8
9
#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; \
}

使用案例:

1
ONCE(NSSet *,AllowedFileTypes,[NSSet setWithArray:@[ @"mp3",@"m4a",@"aiff" ]])

然后,你可以调用 AllowedFileTypes() 来获取集合,并根据需要高效创建集合。如果类型不巧包括括号,添加括号就能运行。

结论

仅仅写这个宏,我就发现了很多艰涩的知识。我希望接触这些知识也不会影响你的思维。请谨慎使用这些知识。

今天就这样。以后还会有更多令人兴奋的探索,可能比这还要再不可思议。在此之前,如果你对此主题有任何建议,请发送给 我们

  Total:    No.