你好,我是雨乐~
今天,我们聊聊常量二兄弟(且允许我这么称呼)constexpr & consteval
,本来想称之为常量双胞胎
的,奈何这俩年龄差距实在太大,constexpr诞生于C++11,consteval则存在于C++20。
前情提要
就跟电影或者电视剧一样,在每集的开头都会有个前情提要,把上一集的内容用一分钟时间总结下,方便能更快的进入主题。
关键字 constexpr 是在 C++11 中引入的,并在 C++14 中进行了改进。 它表示常量表达式。 与 const 一样,它可以应用于变量:当任何代码尝试修改值时,都会引发编译器错误。 与 const 不同,constexpr 还可以应用于函数和类构造函数。 constexpr 指示该值或返回值是常量,并且尽可能在编译时计算。
我们且先聊聊哥哥constexpr吧,先从一个示例入手:
#include <iostream>
constexpr int fibonacci(int n) {
if(n ==0)return0;
if(n ==1)return1;
returnfibonacci(n -1)+fibonacci(n -2);
}
int main() {
// 编译时求值
constexprint val1 =fibonacci(10);// 编译时计算 F(10)
// 运行时求值
int n =5;
int val2 =fibonacci(n);// 运行时计算 F(5)
std::cout <<"F(10) = "<< val1 << std::endl;// 输出 55
std::cout <<"F(5) = "<< val2 << std::endl;// 输出 5
return0;
}
这个示例很简单,就是一个简单的斐波那契数列
算式。
正如在示例中注释的那样,fibonacci(10)在编译阶段计算完成,而fibonacci(n)则在运行时计算,可以通过下面的汇编代码来确定:
push rbp
mov rbp, rsp
sub rsp,16
mov DWORD PTR [rbp-4],55
mov DWORD PTR [rbp-8],5
mov eax, DWORD PTR [rbp-8]
mov edi, eax
call fibonacci(int)
mov DWORD PTR [rbp-12], eax
mov eax,0
leave
ret
上面汇编代码中mov DWORD PTR [rbp-4], 55表示将常量值55赋值给某个变量,进一步佐证了我们上面的结论fibonacci(10)在编译阶段完成计算。
consteval
在上节,通过一个示例简单的介绍了constexpr
的用法,对其总结如下:
•如果语境为常量,那么会在编译阶段进行求值•如果语境为非常量,那么会在运行阶段求
我们试着把前面的例子进行修改,使用consteval:
#include <iostream>
consteval int fibonacci(int n) {
if(n ==0)return0;
if(n ==1)return1;
returnfibonacci(n -1)+fibonacci(n -2);
}
int main() {
// 编译时求值
constexprint val1 =fibonacci(10);// 编译时计算 F(10)
// 运行时求值
int n =5;
int val2 =fibonacci(n);// Error
std::cout <<"F(10) = "<< val1 << std::endl;// 输出 55
// std::cout << "F(5) = " << val2 << std::endl; // 输出 5
return0;
}
编译器报错如下:
<source>: In function 'int main()':
<source>:14:25: error: call to consteval function 'fibonacci(n)' is not a constant expression
14 | int val2 = fibonacci(n);
| ~~~~~~~~~^~~
<source>:14:25: error: the value of 'n' is not usable in a constant expression
嗯,从上面报错可以看出,因为n不是一个常量,而consteval表达式的充分条件是其参数必须为常量。
从表明上看,consteval像是对constexpr的一个严格限制,即仅仅要求且只能在编译期进行求值。
Mix
先看下面一个示例:
consteval auto square(int x) {
return x * x;
}
consteval auto twice_square(int x) {
returnsquare(x);// #1
}
int main() {
auto&&a =twice_square(10);
}
嗯,上述编译正常。
留意前面#1处,按照consteval语义的限制,传入#1处的x是个非常量表达式,编译器应该报错才对。这种情况下,标准是允许的,也就是说只要整个函数是在常量表达式的语境下。
下面,对上述代码进行些许改动,如下:
consteval auto square(int x) {
return x * x;
}
constexpr auto twice_square(int x) {// 此处为constexpr
returnsquare(x);
}
int main() {
auto&&a =twice_square(10);
}
注意留意上面注释。
编译器报错如下:
<source>: In function 'constexpr int twice_square(int)':
<source>:6:22: error: call to consteval function 'square(x)' is not a constant expression
6 | return square(x);
| ~~~~~~^~~
<source>:6:22: error: 'x' is not a constant expression
在前面的文章中有提到:consteval要求函数参数为常量表达式,而在上述示例中,constexpr声明的函数允许其变量为非常量表达式,因此在这里x当作非常量表达式,这也就是导致上述编译报错的根本原因。
但是,反过来,即twice_square声明为consteval,而square声明为constexpr,这样就能编译正常,即:
consteexpr auto square(int x) {
return x * x;
}
consteval auto twice_square(int x) {// 此处为constexpr
returnsquare(x);
}
int main() {
auto&&a =twice_square(10);
}
函数指针&lambda
假设存在这样一个函数,如下:
consteval int select(auto f, int a, int b, int c) {
return f(f(a, b), c);
}
其中f是一个可调用(callable)对象,可以是函数指针,也可以是lambda。
现在假如存在一个函数max求两个数之间的大值,以及一个lambda表达式求两个数之间的小值:
consteval int select(auto f, int a, int b, int c) {
returnf(f(a, b), c);
}
consteval int max(int a, int b) {
return a > b ? a : b;
}
int main() {
constexprint a =select(max,34,56,78);
auto min =[](int a,int b){return a < b ? a : b;};
int b =select3(min,34,56,78);
}
上述代码编译正常,也就是说对于consteval函数的函数指针以及非捕获(non-capture)都可以在编译阶段被使用求值,换句话说const int max()函数可以和lambda min一起用于编译阶段求值。
if consteval
对于Mix一节中编译错误的实例,C++23引入了if consteval来进行解决,使用方式如下:
consteval auto square(int x) {
return x * x;
}
constexpr auto twice_square(int x) {
ifconsteval{
returnsquare(x);
}
return x * x;
}
int main() {
int b =10;
auto&&a =twice_square(10);
}
留意twice_square这一行,if consteval
,它允许你根据当前是否处于编译时环境来选择不同的代码路径,如果当前语境是常量表达式,那么就会走square分支,否则走默认分支。
当然了,我们还可以使用其他方式使得那个例子编译通过:
#define constexpr_value(X) \
[]{ \
struct{ \
consteval operatordecltype(X)()const noexcept { \
return X; \
} \
usingconstexpr_value_t=void; \
} val; \
return val; \
}()
template<typename T,typename U>
concept compile_time =
requires{typename T::constexpr_value_t;}
and std::convertible_to<T, U>;
consteval auto square(int x) -> int {
return x * x;
}
constexpr auto twice_square(compile_time<int> auto x) -> int {
ifconsteval{
returnsquare(x);
}
return x * x;
}
int main() {
auto&&a =twice_square(constexpr_value(10));
}
核心宗旨是通过宏constexpr_value创建一个具有 consteval
转换操作符的结构体,返回 X
的常量值。
如果对本文有异议或者有其他技术问题,可以加微信交流:
3、手撸一个线程池