章节目录

Constexpr

本节阅读量:

constexpr 关键字

当使用 const 关键字声明 const 变量时,编译器会隐式地跟踪它是运行时常量还是编译时常量。在大多数情况下,这个区别除了影响优化之外并不重要,但在某些场景下 C++ 需要常量表达式(相关主题会在后续章节介绍),而常量表达式中只能使用编译时常量。

由于编译时常量还允许更好的优化(且几乎没有缺点),因此我们通常希望尽可能多地使用编译时常量。

使用 const 时,变量到底是编译时常量还是运行时常量,取决于其初始值是否为编译时常量表达式。在某些情况下,这并不容易一眼看出。

例如:

1
2
3
4
int x { 5 };       // 非 const
const int y { x }; // 运行时常量 (因为使用非 const 变量初始化)
const int z { 5 }; // 编译时常量
const int w { getValue() }; // 不容易判断

在上面的示例中,w 可能是运行时常量,也可能是编译时常量,这取决于 getValue() 是如何定义的。完全看不出来!

幸运的是,我们可以借助编译器来确保在需要的地方获得编译时常量。为此,我们在声明变量时使用 constexpr 关键字来代替 const。constexpr(是“constant expression”的缩写)变量只能是编译时常量。如果 constexpr 变量的初始化值不是常量表达式,编译器就会报错。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

int five()
{
    return 5;
}

int main()
{
    constexpr double gravity { 9.8 }; // ok: 9.8 是常量表达式
    constexpr int sum { 4 + 5 };      // ok: 4 + 5 是常量表达式
    constexpr int something { sum };  // ok: sum 是常量表达式

    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;

    constexpr int myAge { age };      // 编译报错: age 不是常量表达式
    constexpr int f { five() };       // 编译报错: five() 的返回值不是常量表达式

    return 0;
}

const 和 constexpr 的函数参数

普通函数调用是在运行时求值的。这意味着即使实参本身是编译时常量,函数参数也会被当作运行时常量来处理。

由于 constexpr 对象必须使用编译时常量(而非运行时常量)来初始化,所以不能将函数参数声明为 constexpr。


常量表达式究竟何时被求值?

当上下文要求常量表达式的结果必须是一个常量时(例如用于编译时常量的初始化),编译器就会对常量表达式进行求值:

1
2
constexpr int x { 3 + 4 }; // 3 + 4 必然在编译时计算
const int x { 3 + 4 };     // 3 + 4 必然在编译时计算

在不要求结果是常量表达式的上下文中,编译器可以自行选择是在编译时还是运行时计算该常量表达式。

1
int x { 3 + 4 }; // 3 + 4 可能在编译时计算,也可能在运行时计算

在上面的变量定义中,x 并不是 constexpr 变量,并且在编译时也不需要知道它的初始值。因此,编译器可以自行决定是在编译时还是运行时计算 3+4。

尽管标准并未严格要求,但现代编译器通常会选择在编译时计算常量表达式,因为这样做可以带来更好的性能。


常量折叠(Constant folding)

来看下面的示例:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
	constexpr int x { 3 + 4 }; // 3 + 4 是常量表达式
	std::cout << x << '\n';    // 这是一个运行时表达式

	return 0;
}

3+4 是一个常量表达式,编译器会在编译时计算出 3+4 并将其替换为值 7。由于 x 是编译时常量,编译器可能会在上述程序中把 x 完全优化掉,将 std::cout << x << '\n' 替换为 std::cout << 7 << '\n'。最终的输出表达式会在运行时执行。

不过,既然 x 只使用了一次,我们更可能一开始就把程序写成这样:

1
2
3
4
5
6
7
#include <iostream>

int main()
{
	std::cout << 3 + 4 << '\n'; // 这是一个运行时表达式
	return 0;
}

由于表达式 std::cout << 3 + 4 << '\n' 并不是常量表达式,那么你可能会怀疑:其中的常量子表达式 3+4 是否仍然会被编译时优化。

答案通常是“会”。编译器很早就具备了优化常量子表达式的能力,即使整个完整表达式是运行时表达式也是如此。这个优化过程被称为“常量折叠”。

将变量声明为 constexpr,可以确保当它们被用在常量子表达式中时能够符合常量折叠的条件。


5.1 常量表达式、编译时常量和运行时常量

上一节

5.3 字面值常量

下一节