C++之constexpr详解

发布时间 2023-05-25 09:24:18作者: imxiangzi

constexpr表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。声明为constexpr的变量一定是一个const变量,而且必须用常量表达式初始化:

constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); //之后当size是一个constexpr函数时才是一条正确的声明语句
constexpr 变量

必须明确一点,在constexpr声明中如果定义了一个指针,限定符conxtexpr仅对指针有效,与指针所指的对象无关。

const int*p = nullptr; //p是一个指向整形常量的指针
constexpr int* q = nullptr; //q是一个指向整数的常量指针
p是一个指向常量的指针,q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const。

例在微软编译器上:

#include <iostream>

int main()
{
int i = 10;
int j = 100;

std::cout << "i=" << i << std::endl;

constexpr int* p = &i;
*p = 8;

std::cout << "i=" << i << std::endl;

//p = &j; //error

return 0;
}
结果如下:

 

使用GNU gcc编译器时,constexpr指针所指变量必须是全局变量或者static变量(既存储在静态数据区的变量)。

#include <iostream>

int main()
{
static int bufSize = 512;

std::cout << "bufSize=" << bufSize << std::endl;

constexpr int* ptr = &bufSize;
*ptr = 1024;
std::cout << "bufSize=" << bufSize << std::endl;

return 0;
}
ps:全局变量和局部变量的存储区域不同,全局变量存放在静态数据区,局部变量存放在栈区。但还有一个小点就是存放在静态数据区的变量是由低地址向高地址存放的,但存放在栈区的变量却是由高地址向低地址存放的,存放在静态数据区的还有静态局部变量和静态全局变量。

constexpr int ver = 4;switch...case tag中,这个ver变量可以作为tag;

#include <iostream>

int main(void)
{
constexpr int ver = 4;
int v = 4;
switch(v)
{
case ver:
std::cout << "ver is 4" << std::endl;
break;
default:
std::cout << "ver is default" << std::endl;
}
}
constexpr int size = 10; size可以用在需要编译时就能确定的代码中:

#include <iostream>
#include <array>

int main(void)
{
constexpr int size = 10;
std::array<int, size> arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
for(const auto i : arr)
{
std::cout << i << ' ';
}
}
constexpr定义的变量值必须由常量表达式初始化;,constexpr是一个加强版的const,它不仅要求常量表达式是常量,并且要求是一个编译阶段就能够确定其值的常量。

#include <iostream>

int main(void)
{
int x = 42;
const int size = x;
int buffer[size] = {};//clang: error: variable-sized object may not be initialized

}
上述代码虽然size初始化编译成功, 但是编译器并不一定把它作为一个编译期需要确定的值,所在在GCC上能够编译通过,在clang和MSVC上编译失败;如果把const替换为constexpr,会有不
同的情况发生:

#include <iostream>

int main(void)
{
int x = 42;
constexpr int size = x;//error: constexpr variable 'size' must be initialized by a constant expression
int buffer[size] = {};

}
constexpr支持声明浮点数类型的常量表达式而且标准还规定其精度必须至少和运行时的精度相同,例如:

#include <iostream>

constexpr double sum(double d)
{
return d > 0 ? d + sum(d - 1) : 0;
}
int main(void)
{
constexpr double d = sum(5);
std::cout << d;
}
constexpr的inline属性
在C++17标准中,constexpr声明静态成员变量时,也被赋予了该变量的内联属性。

class X
{
public:
static constexpr int num{ 5 };
};
以上代码从C++17开始等价于:

class X
{
public:
inline static constexpr int num{ 5 };
};
在这里X::num既是申明又是定义;可以通过如下代码来测试:

#include <iostream>
class X
{
public:
static constexpr int num{ 5 };
};

int main()
{
auto* ptr = &X::num;//这里取地址符需要有变量的定义
std::cout << *ptr << std::endl;
}
if constexpr
1.if constexpr的条件必须是编译期能确定结果的常量表达式。
2.条件结果一旦确定,编译器将只编译符合条件的代码块。

详细请见编译时期if

constexpr 函数
constexpr还能定义一个常量表达式函数,即constexpr函数,常量表达式函数的返回值可以在编译
阶段就计算出来。不过在定义常量表示函数的时候,我们会遇到更多的约束规则[C++14]:

1.函数体允许声明变量,除了没有初始化、static和thread_local变量。
2.函数允许出现if和switch语句,不能使用goto语句。
3.函数允许所有的循环语句,包括for、while、do-while。
4.函数可以修改生命周期和常量表达式相同的对象。
5.函数的返回值可以声明为void。
6.constexpr声明的成员函数不再具有const属性。

另外还有些不允许的:

a. 它必须非虚; [c++20前]
b. 它的函数体不能是函数 try 块; [c++20前]
c. 它不能是协程; [c++20起]
d. 对于构造函数与析构函数 [C++20 起],该类必须无虚基类
e. 它的返回类型(如果存在)和每个参数都必须是字面类型 (LiteralType)
f. 至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函 数为足以用于常量初始化器) (C++14 起)。不要求诊断是否违反这点。

#include <iostream>

constexpr int abs_(int x)
{
if (x > 0)
{
return x;
}
else
{
return -x;
}
}
constexpr int sum(int x)
{
int result = 0;
while (x > 0)
{
result += x--;
}
return result;
}
constexpr int next(int x)
{
return ++x;
}

int main()
{
char buffer1[sum(5)] = { 0 };
char buffer2[abs_(-5)] = { 0 };
char buffer3[next(5)] = { 0 };
}

这里可以看到在next函数中++x可以改变传入参数的值,这是基于第4点:函数可以修改生命周期和常量表达式相同的对象。

需要强调一点的是,虽然常量表达式函数的返回值可以在编译期计算出来,但是这个行为并不是确定的。例如,当带形参的常量表达式函数接受了一个非常量实参时,常量表达式函数可能会退化为普通函数:

如果我们传入常量表达式的实参,constexpr函数在编译时期就计算出来;

#include <iostream>
#include <array>

constexpr int size(int i)
{
return i*2;
}

int main(void)
{
std::array<int, size(10)> arr;//传入参数是常量实参,此时constexpr普通函数在编译期就计算出来了
std::cout << arr.size() << std::endl;
}
传入实参为非常量表达式时,退化为普通函数:

#include <iostream>

constexpr int size(int i)
{
return i*2;
}

int main(void)
{
int i = 10;
constexpr int s = size(i);//编译错误:传入参数接受了一个非常量实参,此时constexpr退化为普通函数了
}
这种退化机制对于程序员来说是非常友好的,它意味着我们不用为了同时满足编译期和运行期计算而定义两个相似的函数。另外,这里也存在着不确定性,因为GCC依然能在编译阶段计算size
的结果,但是MSVC和CLang则不行。

constexpr和class
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说明符就可以编译成功,因为它们本身都符合常量表达式构造函数和常量表达式函数的要求,我们称这样的类为字面量类类型
(literal class type)。

对于constexpr int get()const 函数,在C++11中,constexpr会自动给函数带上const属性。而从C++14起constexpr返回类型的类成员函数不在是const函数了;

请注意,常量表达式构造函数拥有和常量表达式函数相同的退化特性,当它的实参不是常量表达式的时候,构造函数可以退化为普通构造函数,当然,这么做的前提是类型的声明对象不能为常量表达式值:

int i = 8;
constexpr X x(i); // 编译失败,不能使用constexpr声明
X y(i); // 编译成功
由于i不是一个常量,因此X的常量表达式构造函数退化为普通构造函数,这时对象x不能用constexpr声明,否则编译失败。

C++14对constexpr规则的修改同样也影响到了constexpr构造函数:

#include <iostream>
class X {
public:
constexpr X() : value(5) {}
constexpr X(int i) : value(0)
{
if (i > 0)
{
value = 5;
}
else
{
value = 8;
}
}
constexpr void set(int i)
{
value = i;
}
constexpr int get() const
{
return value;
}
private:
int value;
};

constexpr X make_x()
{
X x;
x.set(42);
return x;
}

int main()
{
constexpr X x1(-1);
constexpr X x2 = make_x();
constexpr int a1 = x1.get();
constexpr int a2 = x2.get();
std::cout << a1 << std::endl;
std::cout << a2 << std::endl;
}

constexpr声明的x1、x2、a1和a2都是编译期必须确定的值。constexpr构造函数内可以使用if语句并且对value进行赋值操作。根据规则5:函数的返回值可以声明为void:返回类型为void的set函数也被声明为constexpr,这也意味着该函数能够运用在constexpr声明的函数体内,make_x函数就是利用了这个特性。

根据规则4:函数可以修改生命周期和常量表达式相同的对象。 和规则6:constexpr声明的成员函数不再具有const属性。set函数也能成功地修改value的值。

使用constexpr声明自定义类型的变量,必须确保这个自定义类型的析构函数是平凡的,否则也是无法通过编译的。平凡析构函数必须满足下面3个条件。
1.自定义类型中不能有用户自定义的析构函数。
2.析构函数不能是虚函数。
3.基类和成员的析构函数必须都是平凡的。

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对
象还是没有问题的。GCC在这种情况下依然能够在编译阶段求出get_size的值,MSVC和CLang则不行。对于情况2,由于static变量的存在,lambda表达式对象get_count不可能在编译期运算,因此它最终会在运行时计算。
值得注意的是,我们也可以强制要求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();
参考:

现代C++语言核心特性解析

C++17 The Complete guide
————————————————
版权声明:本文为CSDN博主「C咖咖」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/janeqi1987/article/details/103542802