4

C++编译时的“变量”(上篇)

 2 years ago
source link: http://hczhcz.github.io/2015/08/24/c++-compile-time-variable.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

编译时的immutable

有人评价C++是一门“两门语言”。因为它除了C with Classes以外,还通过模板元编程等特性,在编译时提供了一门图灵完全的、独立于运行时的语言。

C++的模板元编程几乎是纯粹的函数式语言,它与C++中C的那部分风格迥异——我们可以在模板中看到递归(而非控制流)、模式匹配、记忆化和鸭子类型的影子。

当然,这就意味着模板元编程是鼓励immutable(不可变)的。我们不能重复定义模板,也不能在使用模板之后对模板进行特化。同样的事情发生在编译时的方方面面——我们不能随意修改类型、重新定义变量、blabla。immutable的特性意味着更大的安全性——如果仅仅改变了声明、定义的顺序,编译器就会产生另一个合法却有着不同行为的程序,那会带来不小的混乱。这是C++模板相较于从C继承来的宏的一大优势。

看起来严丝合缝?但是……C++偏偏在编译时机制中开了一道有趣的缺口。事情要从函数重载说起。

函数重载选取机制

C++支持函数重载是众所周知的,然而C++的重载机制比大多数人想象的都要复杂。

考虑表达式f()(注:这里不讨论宏)。

首先,我们要找到所有的f,包括:

  • 当前作用域下的f函数(包括模板);
  • 来自其它作用域的,可达的f函数,包括上层、全局命名空间,以及using等情形;
  • ADL规则确定的、各参数所在作用域可达的f函数;
  • (如果在一个对象内)对象方法调用this->f()
  • 当前及其它作用域下的,变量foperator()
  • 当前及其它作用域下的,变量f可隐式类型转换到的函数;
  • 当前及其它作用域下的,到类型f的类型转换(构造)。

根据C++的惯例,f的寻找总是向前的。这一点在实现“编译时变量”的过程中会被用到。

根据上面找到的f所在的作用域等,编译器会推断这是一个函数调用、operator()调用,还是类型转换。然后,排除掉不可用的f(这里会涉及到下面将提到的SFINAE),即可得到备选f的集合。

从备选集合选出最终的重载函数的过程涉及到许多复杂的细节。这里只能给出一个大体的规则:

  • 参数所需的隐式类型转换更优(层次更浅),则优先选取;
  • 参数所需的标准类型转换(构造)更优,则优先选取;
  • 优先选取非模板;
  • 优先选取对参数的特化程度更高的模板。

SFINAE

SFINAE,即“Substitution Failure Is Not An Error”。

这是C++函数重载的一条规则。当某个函数名对应多个函数(函数模板),编译器会忽略那些无法代入的重载函数,而不报错。

举个例子,我们可以利用重载函数检测一个值是不是指针:

 1 template <class T>
 2 bool is_pointer(T *) {
 3     return true;
 4 }
 5 
 6 template <class Object, class T>
 7 bool is_pointer(T Object::*) {
 8     return true;
 9 }
10 
11 template <class T>
12 bool is_pointer(T (*)()) {
13     return true;
14 }
15 
16 bool is_pointer(std::nullptr_t) {
17     return true;
18 }
19 
20 bool is_pointer(...) {
21     return false;
22 }

is_pointer传入一个int *时,编译器会忽略第二个重载(无法推导Object的类型)、第三个重载(不是函数指针)、第四个重载(不是std::nullptr_t)。

按照C++重载函数的选取顺序,第一个重载优于第五个重载,因此,调用后返回true。同理,我们可以用is_pointer来识别各种指针。

另外,C++11带来了一种新的玩法——表达式的SFINAE:

1 template <class T>
2 auto looks_like_pointer(T value) -> decltype(*value, true) {
3     return true;
4 }
5 
6 bool looks_like_pointer(...) {
7     return false;
8 }

SFINAE十分有用,它不仅可以用来选择函数,还可以负责一些编译时的计算工作。如果上面的例子中,函数的返回不全为bool,而是分别为floatdouble,我们就能通过sizeof在编译时判断某个类型是不是指针。

SFINAE也可以用来检测某个类是否拥有特定的成员:

1 template <class T>
2 auto has_member1(T &value) -> decltype((void) value.member1) {
3     // has T::member1
4 }

或者判断某个数的奇偶:

1 template<int I>
2 void is_even(int (&)[I % 2 == 0]) {
3     // even
4 }
5 
6 template<int I>
7 void is_even(int (&)[I % 2 == 1]) {
8     // odd
9 }

C++11中提供了std::enable_if类,可以达到与上例类似的效果。

下篇中,我们将看到,如何利用函数重载选取机制和SFINAE来实现编译时的“变量”。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK