自学内容网 自学内容网

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 X
    char 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 表达式,它包含一个静态局部变量 xstatic 变量在第一次调用时初始化,并在之后的每次调用中保持其值。所以 get_count 返回的值是 x,而 x 是静态的,因此 get_count() 会返回一个固定值。static变量并不符合constexpr的要求,因为constexpr需要在编译时就能求值,而static变量的生命周期是运行时管理的。如果尝试将这个 lambda 声明为constexpr,会导致编译失败。


原文地址:https://blog.csdn.net/m0_52043808/article/details/144016608

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!