章节目录

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

本节阅读量:

在上一课中,介绍了如何使用const关键字使变量成为值不能更改的常变量。

在本课中,我们将查看常量的另一个属性:它们是运行时常量还是编译时常量。


as-if规则

在C++中,编译器有很大的余地来优化程序。as-if 规则说,编译器可以随意修改程序,以产生更优化的代码,只要这些修改不影响程序的“可观察行为”。

编译器优化的确切方式取决于编译器本身。然而,我们可以做一些事情来帮助编译器更好地优化。


优化机会

考虑以下程序:

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

int main()
{
	int x { 3 + 4 };
	std::cout << x << '\n';

	return 0;
}

输出很简单:

1
7

然而,其中隐藏着一种有趣的优化可能性。

如果此程序完全按照编写的方式编译(没有优化),编译器将生成一个可执行文件,该可执行文件在运行时(当程序运行时)计算3+4的结果。如果程序执行了一百万次,3+4将被求值一百万次;结果值7产生一百万次。

但请注意,3+4的结果永远不会改变——它总是7。因此,每次运行程序时重新计算3+4是严重浪费的。


常量表达式

常量表达式(constant expression)是编译器可以在编译时计算的表达式。要成为常量表达式,表达式中的所有值都必须在编译时已知(并且调用的所有运算符和函数都必须支持编译时求值)。

当编译器遇到常量表达式时,它可以在编译时计算表达式,然后用计算结果替换常量表达式。

在上面的程序中,表达式 “3 + 4” 是一个常量表达式。因此,当编译该程序时,编译器可以将其替换为结果值7。换句话说,在 as-if 规则下,编译器实际上等价于编译以下内容:

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

int main()
{
	int x { 7 };
	std::cout << x << '\n';

	return 0;
}

该程序产生与以前版本相同的输出(7),但生成的可执行文件不再需要在运行时花费CPU周期来计算3+4!更好的是,我们不需要做任何事情来启用此行为(除了启用优化)。

请注意,表达式「std::cout « x」不是常量表达式,因为我们的程序不能在编译时将值输出到控制台。因此,该表达式将始终在运行时求值。必须在运行时计算的表达式有时称为运行时表达式。


另一个优化机会

上面的程序中还有另一个效率低下的地方:程序为x分配内存,将值7存储在该内存中,然后在随后的语句中访问内存以获得要打印的x(7)的值。由于x的值从不改变,因此这种内存访问是浪费的。

换句话说,在 as-if 规则下,编译器可以将上述程序优化为:

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

int main()
{
	std::cout << 7 << '\n';

	return 0;
}

但为了进行这种优化,编译器必须确保x在定义和使用之间没有变化。由于x不是常量,编译器将不得不自己的分析来确定是否可进行这样的优化。虽然复杂的现代编译器可以做到这一点(至少在这个简单的情况下),但不是所有的编译器或在更复杂的情况下将无法进行这种优化。

与常量表达式优化(本质上是免费的)不同,这种优化可能无法自动进行。

然而,我们可以做少量的工作,帮助编译器,使其更有可能执行这种优化。


编译时常量

编译时常量是其值为常量表达式的常量。字面值(例如 1、2.3 和“Hello,world!”)是编译时常量的一种类型。

常变量可以是也可以不是编译时常量(取决于它们的初始化方式)。


编译时 const

如果常变量的初始值是常量表达式,则常变量是编译时常量。

考虑一个类似于上面的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

int main()
{
	const int x { 3 };  // x 是 编译时常量
	const int y { 4 };  // y 是 编译时常量

	const int z { x + y }; // x + y 是常量表达式, z 是编译时常量

	std::cout << z << '\n'; 

	return 0;
}

由于x和y的初始化值是常量表达式,因此x和y是编译时常量。这意味着x+y也是常量表达式。因此,当编译器编译该程序时,它可以计算x+y的值,并用结果7替换常量表达式。

请注意,编译时常量的初始值设定项可以是任何常量表达式。以下所有变量都是编译时常量变量:

1
2
3
const int z { 3 };     // 3 是常量表达式, z 是编译时常量
const int a { 1 + 2 }; // 1 + 2 是常量表达式, a 是编译时常量
const int b { z * 2 }; // z * 2 是常量表达式, b 是编译时常量

命名常量 通常用作 编译时常变量:

1
const double gravity { 9.8 };

编译时常量优化

编译时常量使编译器能够执行更多的优化。在许多情况下,优化完成后,编译时常变量的访问不会再出现在程序中。例如,每当使用编译时重力常数时,编译器可以简单地将标识符gravity替换为9.8,这避免了必须从内存中的某个位置获取值。

让我们回到前面的例子:

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

int main()
{
	int x { 7 }; // x 不是常量
	std::cout << x << '\n';

	return 0;
}

现在,让我们将x设置为编译时常量:

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

int main()
{
	const int x { 7 }; // x 是 编译时常量
	std::cout << x << '\n';

	return 0;
}

如果我们这样做,编译器就会知道x不会改变,并且很可能会将程序优化为:

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

int main()
{
	std::cout << 7 << '\n';

	return 0;
}

运行时常量

如果常变量的初始值是非常量表达式,则是运行时常量。运行时常量是在运行时之前无法确定其初始化值的常量。

下面的示例说明了作为运行时常量的使用:

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

int getNumber()
{
    std::cout << "Enter a number: ";
    int y{};
    std::cin >> y;

    return y;  
}

int main()
{
    const int x { 3 };           // x 是编译时常量

    const int y { getNumber() }; // y 是运行时常量

    const int z { x + y };       // x + y 是运行时常量, z 是运行时常量
    
    return 0;
}

即使y有一个常量限定符,但初始值(getNumber() 的返回值)直到运行时才知道。因此,y是运行时常量,而不是编译时常量。由于y是运行时常量,因此必须在运行时计算z,表达式x+y也是运行时表达式。


5.0 常量(命名常量)

上一节

5.2 Constexpr

下一节