6.1 C++11 常量表达式

一、编译时常量性和运行时常量性

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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值