运行时常量性与编译时常量性
常量,表示这个数值不可被修改。在C++11之前都是通过const修饰。const可以修饰函数参数、函数返回值、函数本体(常成员函数)、变量、类等等。但在部分场景下,const表示的常量是运行时常量,只能保证运行时数据不会发生变化。这样就会导致我们需要在编译时的常量性不能给予体现。
例如,下面编写了一个函数GetSize()返回一个被const修饰的整数。这种被const修饰的就仅仅表示的是运行时常量。所以,在使用这个常量数值创建数组时编译失败,当作枚举成员赋值也编译失败,作为swicth中的case 分支也会编译错误。
#include <iostream> using namespace std; const int GetSize() { return 1; } int main() { int arr[GetSize()] = {0}; enum MyEnum { A = GetSize(), }; int x; switch (x) { case GetSize(): break; default: break; } return 0; }
编译报错:
Source.cpp(12,17): error C2131: expression did not evaluate to a constant Source.cpp(12,17): message : failure was caused by call of undefined function or one not declared 'constexpr' Source.cpp(12,17): message : see usage of 'GetSize' Source.cpp(16,14): error C2131: expression did not evaluate to a constant Source.cpp(16,14): message : failure was caused by call of undefined function or one not declared 'constexpr' Source.cpp(16,14): message : see usage of 'GetSize' Source.cpp(21,14): error C2131: expression did not evaluate to a constant Source.cpp(21,14): message : failure was caused by call of undefined function or one not declared 'constexpr' Source.cpp(21,14): message : see usage of 'GetSize' Source.cpp(21,1): error C2051: case expression not constant Source.cpp(25,2): warning C4065: switch statement contains 'default' but no 'case' labels
constexpr常量表达式(C++11)
上述这种场景我们可以通过#define进行处理,但是这种粗暴的方法在C++中有点不合群。为此C++11引入了constexpr,常量表达式(constant expression)。
还是聊刚刚那个例子,我们只需要将原本的const修饰改为constexpr修饰即可实现编译时常量。
constexpr int GetSize() { return 1; }
常量表达式函数(constexpr)
虽然说我们只需要在函数返回类型前加入关键字constexpr
就可以让函数变成常量表达式。但是常量表达式是有一定限制的。
具体要求如下四条:
1. 函数体只有单一的return返回语句
2. 函数必须要有返回值(不能为void)
3. 在使用前必须已有定义
4. return返回语句表达式中不能使用非常量表达式的函数、全局数据,并且必须是一个常量表达式。
最严格的要求毫无疑问就是函数体内只能有单一的return返回语句。例如下面这个Getsize函数中有一行输出就会导致constxpr修饰失败。如果是那种可以被优化掉的语句将不受影响。
constexpr int GetSize() { cout << 1 << endl; return 1; }
函数必须要有返回值其实能理解,因为constexpr修饰的就是返回值,然而没有返回值那么constexpr就没有意义。
在使用前必须已有定义,这个可能不太好理解。对于普通的函数只需要声明即可。而常量表达式函数的使用比较特殊,使用和调用并不一样。前者讲的是编译时值的计算,后者讲的是运行时函数的调用。
下面这段代码中,我们声明了常量表达式函数f()。在定义f函数前,我们定义了变量a、常量b、常量表达式c。a和b的初始化中编译器会将f函数转换为一个函数调用。在c的初始化中,由于c是一个常量表达式,所以编译器进行编译时就会计算出函数的值。然而此时函数f并未定义,所以就会造成编译错误。最后d的初始化时,f函数已经定义过了。所以在编译期可以正常计算出值。
constexpr int f(); int a = f(); const int b = f(); constexpr int c = f(); // 编译错误 constexpr int f() { return 1; } constexpr int d = f();
return语句必须是常量表达式。我们要使其返回值变成一个编译期的常量,而return语句的表达式中包含了运行时才能确定的值,这是互相矛盾的。
例如下面这个例子,返回的表达式中包含了参数就没法在编译期计算出值了。
constexpr GetSize(int z) { return z; }
常量表达式值
常量表达式值必须被一个常量表达式赋值。和常量表达式函数相同,常量表达式值在使用前必须初始化。C++11标准要求,编译时的浮点数常量表达式的精度必须大于等于运行时的浮点数常量的精度(为了解决浮点数精度丢失问题)。
使用const修饰与constexpr修饰变量有声明区别?
const int i = 1; constexpr int j = 1;
分析:在大多数场景下俩种效果相同,而在全局作用域下 编译期一定会为常量i产生数据,对于常量j如果有代码使用它才会产生数据,否则仅仅当作编译期的值。
对于自定义类型的数据,要成为常量表达式值的话,还需要定义自定义常量构造函数(constant-expression constructor)
class T { public: constexpr T(int index) :i(index) { } private: int i; }; int main() { constexpr T t(1); return 0; }
常量表达式的构造函数条件:
- 函数体必须为空
- 初始化列表只能由常量表达式来赋值
例如下面代码,在创建常量表达式值时参数必须是编译期可得出的值。
int xx = 1; constexpr T t(xx);
我们创建了这个T类型的对象,那么该类型的成员也将具有编译期的常量性。
需要注意一点就是常量表达式不能作用于virtual的成员函数中,virtual表示为运行时,constexpr表示为编译期, 二者是冲突的。
当我们在添加了常量表达式构造函数后就必须要添加非常量表达式构造函数的版本,因为常量表达式构造函数也可用于非常量表达式中的类型构造。
常量表达式的其他应用
1、常量表达式还可以用于模板函数,当由于模板的类型不确定性。所以C++11规定,当声明为常量表达式的模板后,某个该模板的实例化结果不满足常量表达式的要求后,constexpr会自动忽略。
2、 常量表达式还支持递归函数,C++11标准的编译器对常量表达式至少支持512层的递归。
C++模板元编程(template meta-programming)
可以在编译期计算递归函数的值。
template<long int num> struct Fibonacci { static const long int val = Fibonacci<num - 1>::val + Fibonacci<num - 2>::val; }; template<> struct Fibonacci<2> { static const long int val = 1; }; template<> struct Fibonacci<1> { static const long int val = 1; }; template<> struct Fibonacci<0> { static const long int 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 (const auto elem : fib) { cout << elem << endl; } return 0; }
那么用constexpr关键字将会极大的降低代码体积。
constexpr int Fibonacci(int n) { return (1 == n) ? 1 : ((2 == n) ? 1 : Fibonacci(n - 1) + Fibonacci(n - 2)); } int main() { int fib[] = { Fibonacci(11), Fibonacci(22), Fibonacci(13), Fibonacci(24), Fibonacci(15), Fibonacci(26), }; for (const auto elem : fib) { cout << elem << endl; } return 0; }