原文: 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 函数只有同时满足
- 所有参数都是常量表达式
- 返回的结果被用于常量表达式(比如用于初始化 constexpr 数据)
才会触发编译时求值。如果只有参数是常量表达式而结果不是,那么是否触发编译时求值取决于具体实现。
C++17 的 constexpr if 虽然严格意义上不是 constexpr 而是 if 的一部分,但既然名字里带 constexpr,也顺便提一下。
constexpr if 的主要用途是简化模板代码(这也意味着除非你是库作者或者模板狂魔,很少会用到)。很多原本需要绕弯借助 SFINAE 或者类型 Tag 来实现,需要拆成 N 个函数的功能,可以借助 constexpr if 写到一个函数里。
(还有一个用途是可以代替类似 #if
的功能,但……我觉得是邪道。)