【Modern Cpp】constexpr、consteval傻傻分不清楚

文摘   2024-12-06 12:06   北京  

你好,我是雨乐~

今天,我们聊聊常量二兄弟(且允许我这么称呼)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 的常量值。

如果对本文有异议或者有其他技术问题,可以加微信交流:

推荐阅读  点击标题可跳转

1、【Modern cpp】常见面试题之move语义

2、C++采坑系列之空指针调用

3、手撸一个线程池


雨乐聊编程
毕业于中国科学技术大学,现任某互联网公司高级技术专家一职。本公众号专注于技术交流,分享日常有意思的技术点、线上问题等,欢迎关注