Constexpr
本节阅读量:constexpr 关键字
当使用 const 关键字声明 const 变量时,编译器会隐式地跟踪它是运行时常量还是编译时常量。在大多数情况下,这个区别除了影响优化之外并不重要,但在某些场景下 C++ 需要常量表达式(相关主题会在后续章节介绍),而常量表达式中只能使用编译时常量。
由于编译时常量还允许更好的优化(且几乎没有缺点),因此我们通常希望尽可能多地使用编译时常量。
使用 const 时,变量到底是编译时常量还是运行时常量,取决于其初始值是否为编译时常量表达式。在某些情况下,这并不容易一眼看出。
例如:
|
|
在上面的示例中,w 可能是运行时常量,也可能是编译时常量,这取决于 getValue() 是如何定义的。完全看不出来!
幸运的是,我们可以借助编译器来确保在需要的地方获得编译时常量。为此,我们在声明变量时使用 constexpr 关键字来代替 const。constexpr(是“constant expression”的缩写)变量只能是编译时常量。如果 constexpr 变量的初始化值不是常量表达式,编译器就会报错。
例如:
|
|
最佳实践
任何在初始化之后不会修改、并且初始值在编译时就能确定的变量,都应声明为 constexpr。任何在初始化之后不会修改、但初始值在编译时无法确定的变量,则应声明为 const。
在后续章节中,我们会介绍一些目前与 constexpr 不兼容的类型(包括 std::string、std::vector 等使用动态内存分配的类型)。对于这些类型的常量对象,请改用 const。
注
本站点上的许多示例是在“使用 constexpr”这一最佳实践确立之前写的——因此,你会看到许多示例并没有遵循上述最佳实践。我们正在逐步更新遇到的不符合规范的示例。
const 和 constexpr 的函数参数
普通函数调用是在运行时求值的。这意味着即使实参本身是编译时常量,函数参数也会被当作运行时常量来处理。
由于 constexpr 对象必须使用编译时常量(而非运行时常量)来初始化,所以不能将函数参数声明为 constexpr。
相关内容
C++ 确实支持可以在编译时求值的函数(因此可以在常量表达式中使用),我们会在后续的——constexpr 和 consteval 函数——一节中讨论。
C++ 还支持把编译时常量传递给函数的方法。我们会在——非类型模板参数——一节中讨论。
常量表达式究竟何时被求值?
当上下文要求常量表达式的结果必须是一个常量时(例如用于编译时常量的初始化),编译器就会对常量表达式进行求值:
|
|
在不要求结果是常量表达式的上下文中,编译器可以自行选择是在编译时还是运行时计算该常量表达式。
|
|
在上面的变量定义中,x 并不是 constexpr 变量,并且在编译时也不需要知道它的初始值。因此,编译器可以自行决定是在编译时还是运行时计算 3+4。
尽管标准并未严格要求,但现代编译器通常会选择在编译时计算常量表达式,因为这样做可以带来更好的性能。
常量折叠(Constant folding)
来看下面的示例:
|
|
3+4 是一个常量表达式,编译器会在编译时计算出 3+4 并将其替换为值 7。由于 x 是编译时常量,编译器可能会在上述程序中把 x 完全优化掉,将 std::cout << x << '\n' 替换为 std::cout << 7 << '\n'。最终的输出表达式会在运行时执行。
不过,既然 x 只使用了一次,我们更可能一开始就把程序写成这样:
|
|
由于表达式 std::cout << 3 + 4 << '\n' 并不是常量表达式,那么你可能会怀疑:其中的常量子表达式 3+4 是否仍然会被编译时优化。
答案通常是“会”。编译器很早就具备了优化常量子表达式的能力,即使整个完整表达式是运行时表达式也是如此。这个优化过程被称为“常量折叠”。
将变量声明为 constexpr,可以确保当它们被用在常量子表达式中时能够符合常量折叠的条件。