一、编译时常量性和运行时常量性
C++11以前提供了const关键字,可以用于修饰变量、函数等,使得对应的内容具备运行时常量性,
但是面对数组长度、case、enum等场景,const关键字无法满足(因为此时需要编译时常量)。
const int GetConst() { return 1; }
void Constless(int cond) {
int arr[GetConst()] = {0}; // 无法通过编译
enum { e1 = GetConst(), e2 }; // 无法通过编译
switch (cond) {
case GetConst(): // 无法通过编译
break;
default:
break;
}
}
当然我们可以使用宏代替,但这无疑会把C++拉回“石器时代”
#define GetConst 1
C++11提供了constexpr关键字(常量表达式),从而使得被修饰的内容获得编译时常量性。
二、常量表达式函数
将在函数表达式添加constexpr关键字就可以声明一个常量表达式函数,除此之外对函数还有一些要求:
- 只有单一的return语句(可以有一些无关紧要的语句,如断言等,但是不能有实际意义的语句)
- 函数必须返回值
- 使用前必须有定义
- return语句不能是非常量表达式函数、值,且必须是常量表达式
关于第一条,可以看下面的例子,其中static_assert不会产生影响,而定义变量的语句则会导致编译不过。
constexpr int data() { const int i = 1; return i; } //不允许
//允许
constexpr int f(int x){
static_assert(0 == 0, "assert fail.");
return x;
}
而对于第三条而言,需要注意使用而非调用。
constexpr int f();
int a = f();
const int b = f();
constexpr int c = f(); // 无法通过编译
constexpr int f() { return 1; }
constexpr int d = f();
上方第2,3行对f()的调用并不会产生问题(函数f()在第一行声明,而在第5行定义),但是在第4行使用其编译时常量性则会导致编译失败
对于第四条要求,也是非常重要的,即常量表达式函数的调用链上必须都是常量表达式,否则编译不过。
const int e() { return 1; }
constexpr int g() { return e(); } //无法调用非constexpr函数e
除此之外一些危险的操作,如赋值表达式也是不允许的。(从C++14放开)
constexpr int k(int x) { return x = 1; }
C++11先编译报错:
error: expression ‘(x = 1)’ is not a constant expression
constexpr int k(int x) { return x = 1; }
C++14下可以编译过。
三、常量表达式值
通过constexpr修饰的变量称为常量表达式值
const int i = 1;
constexpr int j = 1;
事实上,两者在大多数情况下是没有区别的。不过有一点是肯定的,就是如果i在全局名字空间中,编译器一定会为i产生数据。而对于j,如果不是有代码显式地使用了它的地址,编译器可以选择不为它生成数据,而仅将其当做编译时期的值(是不是想起了光有名字没有产生数据的枚举值,以及不会产生数据的右值字面常量?事实上,它们也都只是编译时期的常量)
对于浮点数常量,由于编译时和运行时环境可能不同,这样就会导致浮点数常量的精度可能有所变化,因此在C++11中,要求编译时的浮点常量表达式值的精度要至少等于(或者高于)运行时的浮点数常量的精度。
对于自定义类型constexpr无法直接修饰,需要通过自定义常量构造函数
//无法通过编译
constexpr struct MyType {int i; }
constexpr MyType mt = {0};
//可以通过编译
struct MyType {
constexpr MyType(int x): i(x){}
int i;
};
constexpr MyType mt = {0};
对于常量表达式的构造函数也有一定约束,主要的有以下两点:
- 函数体必须为空。
- 初始化列表只能由常量表达式来赋值。这里其实是指成员变量必须初始化,可以在通过构造时初始化列表,也可以是就地初始化(字面值常量)。
关于第二点,也是比较容易出错的。
int f();
struct MyType { int i; constexpr MyType(): i(f()){}};
此外,我们虽然定义的时常量构造函数,但其编译期常量性体现的是其对象及成员变量,
#include <iostream>
using namespace std;
struct Date {
constexpr Date(int y, int m, int d):
year(y), month(m), day(d) {}
constexpr int GetYear() { return year; }
constexpr int GetMonth() { return month; }
constexpr int GetDay() { return day; }
private:
int year;
int month;
int day;
};
constexpr Date PRCfound {1949, 10, 1};
constexpr int foundmonth = PRCfound.GetMonth();
int main() { cout << foundmonth << endl; } // 10
// 编译选项:g++ -std=c++11-c 6-1-5.cpp
上面的例子中month并未定义成constexpr,但是却可以作为constexpr函数GetMonth()的返回值
最后不允许常量表达式作用于virtual的成员函数
四、其他应用
1.模板
C++11标准规定,当声明为常量表达式的模板函数后,而某个该模板函数的实例化结果不满足常量表达式的需求的话,constexpr会被自动忽略。该实例化后的函数将成为一个普通函数,并且无法用于需要常量表达式的场景,而只能用于普通函数场景:
class A {};
int main()
{
constexpr int a = test(1);
A c;
constexpr A b = test(c); //无法通过编译
constexpr A b = test(A());
A d = test(c);
std::cout << "Hello World!\n";
}
上面代码,当使用字面值常量1作为test的参数时,可以转为常量表达式函数。而对于类型A而言,如果是以变量的形式c传入,则无法用作常量表达式,而使用A()这样的临时值则可以。
2.函数递归
C++11标准中说明,符合C++11标准的编译器对常量表达式函数应该至少支持512层的递归。
而由于constexpr的编译时常量性,使得对于递归的常量表达式函数的使用,并不会带到运行时展开计算,而是在编译时直接计算好结果进行替换。
#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)