constexpr的作用(转)

发布时间 2023-07-15 22:10:43作者: 大黑耗

原文: https://www.zhihu.com/question/274323507

constexpr 的主要用处有

  • 拓宽「常量表达式」的范围
  • 提供显式「要求」表达式编译时(compile-time)求值的方法

为什么要拓宽「常量表达式」的范围,从原本标准库中的很多尴尬之处就可以看出:

比如我们都知道 INT_MAX 是 C 语言的遗物,C++ 则更希望大家使用 std::numeric_limits<int>::max() 来拿 int 型的上限。然而不幸的是,后者是个函数调用而不是常量,使用起来可能需要花更多性能上的心思,没有前者那么自由。

又比如标准文件流,它的构造函数可以带上这样的第二个参数:

std::fstream foo("foo.txt", std::ios::in | std::ios::out);

这个参数是 openmode 类型的,是由实现具体定义的一种 Bitmask 类型。出于类型安全的考虑,通常用枚举值实现而不是整型。但是这样一来就有个问题,同样是写 std::ios::in | std::ios::out,如果用整型的话可以作为常量表达式使用,而为了类型安全考虑换用枚举实现(尤其是重载 | 操作符)后,就再也不可能是常量表达式了。

inline openmode
operator|(openmode lhs, openmode rhs)
{
    return openmode(int_type(lhs) | int_type(rhs));
}

明明是这样简单的函数,可是对它的调用却不是常量表达式。这就让委员会陷入了必须在「类型安全」和「效率」里二选一的尴尬境地。

(也就是说,枚举类型可以保证类型安全,但枚举类型的值不是常量表达式,而整形可以是常量表达式,可以在编译期就进行计算处理,不用在运行的时候计算,效率更高)

标准库里会遇到这样的问题,大家日常使用也会遇到。加之标准委员会很想借此机会把原本标准中对于「常量表达式」(尤其是整型常量表达式)复杂的定义重构简化,引入 constexpr 就很合情合理了。

(此处解释的是把「对某些简单函数的调用」加入「常量表达式」定义的出发点。其它的比如 constexpr 构造函数的用途,套路是一样的,请自行脑补。)

说到这里,「constexpr 函数」并不能和「编译时求值」划等号,它只表达了「这个函数具备这样的能力」

所以才有了 constexpr 的第二个功能:「显式『要求』表达式编译时求值」(这个时候有了它就相当于要求编译时求值,要做到这点就要求给变量初始化的表达时必须是常量表达式。把它放到变量定义前,那么用来初始化这个变量的表达式「必须」是常量表达式,否则报错。

constexpr 函数只有同时满足

  1. 所有参数都是常量表达式
  2. 返回的结果被用于常量表达式(比如用于初始化 constexpr 数据)

才会触发编译时求值。如果只有参数是常量表达式而结果不是,那么是否触发编译时求值取决于具体实现。


C++17 的 constexpr if 虽然严格意义上不是 constexpr 而是 if 的一部分,但既然名字里带 constexpr,也顺便提一下。

constexpr if 的主要用途是简化模板代码(这也意味着除非你是库作者或者模板狂魔,很少会用到)。很多原本需要绕弯借助 SFINAE 或者类型 Tag 来实现,需要拆成 N 个函数的功能,可以借助 constexpr if 写到一个函数里。

(还有一个用途是可以代替类似 #if 的功能,但……我觉得是邪道。)