Modern Effective C++ item 15:尽可能的使用constexpr
constexpr表达式/对象/函数
constexpr表达式值不会改变并且在编译过程就能得到计算结果的表达式。声明为constexpr的变量一定是一个const变量,而且必须用常量表达式初始化:
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1;// mf + 1是常量表达式
constexpr int sz = size(); //之后当size是一个constexpr函数时才是一条正确的声明语句
constexpr声明中如果定义了一个指针,限定符conxtexpr仅对指针有效,与指针所指的对象无关。
const int*p=nullptr;
constexpr int* q = nullptr;
p是一个指向常量的指针,q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const。
constexpr 函数
constexpr还能定义一个常量表达式函数,即constexpr函数,常量表达式函数的返回值可以在编译
阶段就计算出来。当constexpr应用于函数时,表示函数可以在编译期执行,因为参数也是编译期常量。即使某些参数在运行时才确定,constexpr函数也能正常工作,这时它就相当于普通的函数,在运行时计算结。C++11对constexpr函数的实现有严格限制,仅允许单行return语句,但可以通过递归和三元运算符来增加表达力。到了C++14,限制被大幅放宽,允许更复杂的逻辑,包括循环和局部变量(如下)。适合于那些需要在编译期确定值的情况,例如计算数组大小或作为模板参数等。
约束规则[C++14]:
函数体允许声明变量,但是不允许static和thread_local变量。允许if/switch,不能使用goto语句。允许循环语句,包括for/while/do-while、函数可以修改生命周期和常量表达式相同的对象。 函数的返回值可以声明为void。constexpr声明的成员函数不再具有const属性。
规则约束[c++20前]
a. 必须非虚;
b. 函数体不能是函数 try 块; [c++20前]
c. 不能是协程; [c++20起]
d. 对于构造函数与析构函数 [C++20 起],该类必须无虚基类
e. 它返回类型(如果存在)和每个参数都必须是字面类型 (LiteralType)
f. 至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函 数为足以用于常量初始化器) (C++14 起)。不要求诊断是否违反这点。
constexpr int pow(int base, int exp) noexcept {auto result = 1;for (int i = 0; i < exp; ++i) result *= base;return result;
}
constexpr auto numConds = 5; // 实验条件数量
std::array<int, pow(3, numConds)> results; // 结果数组,大小为3^5
定义constexpr函数pow,用于计算幂。numConds是一个constexpr变量,表示实验条件的数量。 results是一个std::array,pow(3, numConds)在编译期计算得出,用于存储实验的可能结果。
#include<iostream>
#include<array>
// 计算绝对值的 constexpr 函数
constexpr int abs_(int x) {return x > 0 ? x : -x;
}
// 计算从1到x的累加和的 constexpr 函数
constexpr int sum(int x) {int result = 0;while (x > 0) {result += x--;}return result;
}
//返回x+1的constexpr 函数
constexpr int next(int x){return ++x; // 注意,这里的 ++x 是前缀自增操作符
}
//主函数
int main() {//使用 constexpr 函数初始化数组大小char buffer1[sum(5)] = {0};//编译期计算char buffer2[abs_(-5)] = {0};//编译期计算char buffer3[next(5)] = {0};//编译期计算//使用常量表达式作为模板参数std::array<int, size(10)> arr;// 编译期计算std::cout<<"Array size:"<<arr.size()<< std::endl;//尝试使用非常量表达式作为 constexpr 函数的参数int i = 10;// 下面这行会导致编译错误,因为 'i' 不是常量表达式// constexpr int s = size(i); // 编译错误return 0;
}
编译错误:
constexpr int s = size(i);// 编译错误
导致编译错误,因为 i 是一个运行时变量,不是常量表达式。因此,size(i) 不能在编译时确定其值,不能用于初始化 constexpr 变量 s。运行时计算:
int s = size(i); // 正常工作,运行时计算
s不是 constexpr 变量,size(i) 的结果可以在运行时计算并赋值给 s。
常量表达式参数
constexpr int s_constexpr = size(10); // 正常工作,编译时计算
正常工作,因为 10 是一个常量表达式,size(10) 可以在编译时计算其结果,并用于初始化 constexpr 变量 s_constexpr。
constexpr对象
constexpr还能够修饰对象。constexpr对象本质上是const对象的加强版,它们不仅在运行时不可变,而且其值必须在编译期确定。放置在只读存储区域,适用于需要整型常量表达式(如数组大小、模板参数等)的场景。虽然所有constexpr对象都是const,但并非所有const对象都能被视为constexpr,因为后者要求其值必须在编译期可得。
#include <iostream>
struct X {int value;
};
int main(){constexpr X x = { 1 };char buffer[x.value] = { 0 };
}
以上代码自定义了一个结构体X,并且使用constexpr声明和初始化了变量x。到目前为止一切顺利,不过有时候我们并不希望成员变量被暴露出来,于是修改了X的结构:
#include <iostream>
class X {
public:X() : value(5) {}int get() const{ return value;}
private:int value;
};
int main(void){constexpr X x; //error: constexpr variable cannot have non-literal type 'const Xchar buffer[x.get()] = { 0 };//无法在编译期计算
}
解决上述问题只需要用constexpr声明X类的构造函数,即声明一个常量表达式构造函数,当然这个构造函数也有一些规则需要遵循。
1构造函数必须用constexpr声明。2构造函数初始化列表中必须是常量表达式。3构造函数的函数体必须为空(这一点基于构造函数没有返回值,所以不存在return expr。
根据这个constexpr构造函数规则修改如下
#include <iostream>
class X {
public:constexpr X():value(5){}constexpr X(int i):value{i} {}constexpr int get() const{return value;}
private:int value;
};
int main(void){constexpr X x; // error: constexpr variable cannot have non-literal type const X.char buffer[x.get()] = { 0 };
}
上面这段代码给构造函数和get函数添加constexpr说明符可以编译成功,它们本身都符合常量表达式构造函数和常量表达式函数的要求,称这样的类为字面量类类型.
在C++11中,constexpr会自动给函数带上const属性。从C++14起constexpr返回类型的类成员函数不在是const函数了。
常量表达式构造函数拥有和常量表达式函数相同的退化特性,当它的实参不是常量表达式的时候,构造函数可以退化为普通构造函数,当然,这么做的前提是类型的声明对象不能为常量表达式值。
int i=8;
constexpr X x(i); //编译失败,不能使用constexpr声明.
X y(i); //编译成功.
由于i不是一个常量,因此X的常量表达式构造函数退化为普通构造函数,这时对象x不能用constexpr声明,否则编译失败。
constexpr lambda
C++17开始,lambda表达式在条件允许的情况下(常量表达式函数的规则)都会隐式声明为constexpr。
#include <iostream>
#include <array>
constexpr int foo(){return [](){return 58;}();
}
auto get_size =[](int i) {return i * 2;};
int main(void){std::array<int,foo()> arr1= { 0 };std::array<int, get_size(5)> arr2= { 0 };
}
lambda表达式却可以用在常量表达式函数和数组长度中,可见该lambda表达式的结果在编译阶段已经计算出来了。实际上这里的[](int i) { return i * 2; }相当于:
class GetSize {
public:constexpr int operator() (int i) const {return i * 2;}
};
当lambda表达式不满足constexpr的条件时,lambda表达式也不会出现编译错误,它会作为运行时lambda表达式存在。
// 情况1
int i = 5;
auto get_size = [](int i) {return i * 2;};
char buffer1[get_size(i)] = {0}; //编译失败,get_size需要运行时调用
int a1 = get_size(i);
// 情况2
auto get_count = []() {static int x = 5;return x;
};
int a2 = get_count();
情况1和常量表达式函数相同,get_size可能会退化为运行时lambda表达式对象。当这种情况发生的时候,get_size的返回值不再具有作为数组长度的能力,但是运行时调用get_size对象还是没有问题的。
get_size
是一个普通的 lambda 表达式,它依赖于传入的参数i
,并在运行时返回i * 2
。当尝试将get_size(i)
用作数组的大小时,编译器需要在编译时确定数组的大小(这是 C++ 标准要求的)。然而,由于get_size
不是constexpr
,它不能在编译时进行求值,所以get_size(i)
不能用于数组的大小。这会导致编译错误,因为get_size(i)
需要在编译时计算,它只是一个运行时 lambda,编译器无法在编译时计算出它的结果。- 可以强制要求lambda表达式是一个常量表达式,用constexpr声明。做好处是可以检查lambda表达式是否有可能是一个常量表达式,如果不能则会编译报错。
auto get_size = [](int i) constexpr -> int { return i*2; };
char buffer2[get_size(5)] = { 0 };
auto get_count = []() constexpr -> int
{static int x = 5; // 编译失败,x是一个static变量return x;
};
int a2 = get_count();
get_count
是一个 lambda 表达式,它包含一个静态局部变量 x
。static
变量在第一次调用时初始化,并在之后的每次调用中保持其值。所以 get_count
返回的值是 x
,而 x
是静态的,因此 get_count()
会返回一个固定值。static
变量并不符合constexpr
的要求,因为constexpr
需要在编译时就能求值,而static
变量的生命周期是运行时管理的。如果尝试将这个 lambda 声明为constexpr
,会导致编译失败。