6.1.4 常量表达式的其他应用
常量表达式是可以用于模板函数的。不过由于模板中类型的不确定性,所以模板函数是否会被实例化为一个能够满足编译时常量性的版本通常也是未知的。针对这种情况,C++11标准规定,当声明为常量表达式的模板函数后,而某个该模板函数的实例化结果不满足常量表达式的需求的话,constexpr会被自动忽略。该实例化后的函数将成为一个普通函数,如代码清单6-6所示。
代码清单6-6
struct NotLiteral{
NotLiteral(){i=5;}
int i;
};
NotLiteral nl;
template<typename T>constexpr T ConstExp(T t){
return t;
}
void g(){
NotLiteral nl;
NotLiteral nl1=ConstExp(nl);
constexpr NotLiteral nl2=ConstExp(nl);//无法通过编译
constexpr int a=ConstExp(1);
}
//编译选项:g++ -std=c++11-c 6-1-6.cpp
在代码清单6-6中,结构体NotLiteral不是一个定义了常量表达式构造函数的类型,因此是不能够声明为常量表达式值的。而模板函数ConstExp一旦以NotLiteral为参数的话,那么其constexpr关键字将被忽略,如nl1变量所示。实例化为ConstExp<NotLiteral>的函数将不是一个常量表达式函数,因此,我们也看到nl2是无法通过编译的(当然,该例中constexpr NotLiteral本来也是不正确的声明)。而在可以实例化为常量表达式函数的时候,ConstExp则可以用于常量表达式值的初始化。比如本例中的a,就是由实例化为ConstExp<int>的常量表达式函数所初始化的。
对于常量表达式的应用,还有一个有趣的问题就是函数递归问题。如果一个函数是递归的,而且它是一个常量表达式函数,那么会发生什么情况呢?事实上,在常量表达式这个特性提出的时候,提出者是不太赞成让常量表达式支持递归的,不过C++11标准中并没有反对常量表达式的递归函数,而且在标准中说明,符合C++11标准的编译器对常量表达式函数应该至少支持512层的递归。
这就带来一些有趣的结果。有的时候,编译器被我们稍微改造一下,或许就能成为程序员的“计算器”。我们来看看代码清单6-7所示的例子。
代码清单6-7
include <iostream>
using namespace std;
constexpr int Fibonacci(int n){
return(n==1)?1:((n==2)?1:Fibonacci(n-1)+Fibonacci(n-2));
}
int main(){
int fib[]={
Fibonacci(11),Fibonacci(12),
Fibonacci(13),Fibonacci(14),
Fibonacci(15),Fibonacci(16)
};
for(int i:fib)cout<<i<<endl;
}
//编译选项:clang++-std=c++11 6-1-7.cpp
这里程序员知道斐波那契数的算法,却懒得自行算出一个斐波那契数组(第11~16个),因此他利用了常量表达式构造了一个这样的数组。在我们的实验机上,我们用clang++编译器使用默认优化级别编译了这个程序,然后反汇编发现该数组的值已经被计算好了,实际运行的代码没有调用Fibonacci这个函数。这跟直接调用基于范围的for循环打印数组中的值的代码一致。
事实上,这种基于编译时期的运算的编程方式在C++中并不是第一次出现。早在C++模板刚出现的时候,就出现了基于模板的编译时期运算的编程方式,这种编程通常被称为模板元编程(template meta-programming)。模板元编程同样是非常有意思的话题,比如我们要实现代码清单6-7所示例子中的效果,使用模板元编程同样是可以的,如代码清单6-8所示。
代码清单6-8
include <iostream>
using namespace std;
template<long num>
struct Fibonacci{
static const long val=Fibonacci<num-1>::val+Fibonacci<num-2>::val;
};
template<>struct Fibonacci<2>{static const long val=1;};
template<>struct Fibonacci<1>{static const long val=1;};
template<>struct Fibonacci<0>{static const long val=0;};
int main(){
int fib[]={
Fibonacci<11>::val,Fibonacci<12>::val,
Fibonacci<13>::val,Fibonacci<14>::val,
Fibonacci<15>::val,Fibonacci<16>::val,
};
for(int i:fib)cout<<i<<endl;
}
//编译选项:g++ -std=c++11 6-1-8.cpp
代码清单6-8中我们定义了一个非类型参数的模板Fibonacci。该模板类定义了一个静态变量val,而val的定义方式是递归的。因此模板将会递归地进行推导。此外,我们还通过偏特化定义了模板推导的边界条件,即斐波那契的初始值。那么模板在推导到边界条件的时候就会终止推导。通过这样的方法,我们同样可以在编译时进行值计算,从而生成数组的值。
通过constexpr进行的运行时值计算,跟模板元编程非常类似。因此有的程序员自然地称利用constexpr进行编译时期运算的编程方式为constexpr元编程(constexpr meta-programming)。学术地讲,constexpr元编程与template元编程一样,都是图灵完备的,即任何程序中需要表达的计算,都可以通过constexpr元编程的方式来表达。由于constexpr支持浮点数运算(模板元编程只支持整型),支持三元表达式、逗号表达式,所以很多人认为constexpr元编程将会比模板元编程更加强大。从这个角度讲,constexpr元编程将非常让人期待。
不过在这里我们还是必须为这些期待泼点冷水。因为从我们的实践中发现,并不是使用了constexpr,编译器就一定会在编译时期对常量表达式函数进行值计算。事实上,对于代码清单6-4所示的例子,如果用g++的默认优化级别来编译,我们实验机上会产生调用Fibonacci函数的代码(clang++在O0级别也会有这样的效果)。在C++11标准中,我们也没有看到要求编译器一定要对常量表达式进行编译时期的值计算。标准只是定义了可以用于编译时进行值运算的常量表达式的定义,却没有强制要求编译器一定在编译时进行值运算。所以编译器通过一些手段绕过代码的语法,仍然做运行时的调用,这样的方法也是符合C++11标准的(通常情况下,这样做也是编译器实现constexpr的第一步,因为这样最简单也最不容易出错,后期如果实现了编译时值计算,该方法还可以用作验证手段)。推迟到运行时的唯一的缺点就是性能上会有一定问题。可以想象,为了提高编译器的竞争力,各种编译器都会渐渐将常量表达式的运算放到编译时,到那个时候,constexpr元编程或许才能大行其道。