常量表达式、编译时常量和运行时常量
本节阅读量:在上一课中,介绍了如何使用const关键字使变量成为值不能更改的常变量。
在本课中,我们将查看常量的另一个属性:它们是运行时常量还是编译时常量。
as-if规则
在C++中,编译器有很大的余地来优化程序。as-if 规则说,编译器可以随意修改程序,以产生更优化的代码,只要这些修改不影响程序的“可观察行为”。
编译器优化的确切方式取决于编译器本身。然而,我们可以做一些事情来帮助编译器更好地优化。
对于高级读者
“as-if”规则有一个例外:即使那些复制构造函数具有可观察的行为,也可以省略(省略)对复制构造函数的不必要调用。
优化机会
考虑以下程序:
|
|
输出很简单:
|
|
然而,其中隐藏着一种有趣的优化可能性。
如果此程序完全按照编写的方式编译(没有优化),编译器将生成一个可执行文件,该可执行文件在运行时(当程序运行时)计算3+4的结果。如果程序执行了一百万次,3+4将被求值一百万次;结果值7产生一百万次。
但请注意,3+4的结果永远不会改变——它总是7。因此,每次运行程序时重新计算3+4是严重浪费的。
常量表达式
常量表达式(constant expression)是编译器可以在编译时计算的表达式。要成为常量表达式,表达式中的所有值都必须在编译时已知(并且调用的所有运算符和函数都必须支持编译时求值)。
当编译器遇到常量表达式时,它可以在编译时计算表达式,然后用计算结果替换常量表达式。
在上面的程序中,表达式 “3 + 4” 是一个常量表达式。因此,当编译该程序时,编译器可以将其替换为结果值7。换句话说,在 as-if 规则下,编译器实际上等价于编译以下内容:
|
|
该程序产生与以前版本相同的输出(7),但生成的可执行文件不再需要在运行时花费CPU周期来计算3+4!更好的是,我们不需要做任何事情来启用此行为(除了启用优化)。
请注意,表达式「std::cout « x」不是常量表达式,因为我们的程序不能在编译时将值输出到控制台。因此,该表达式将始终在运行时求值。必须在运行时计算的表达式有时称为运行时表达式。
关键点
在编译时计算常量表达式会使编译花费更长的时间(因为编译器必须做更多的工作),但这样的表达式只需要计算一次(而不是每次运行程序时)。生成的可执行文件更快,并且使用更少的内存。
C++执行编译时计算的能力是现代C++最重要和不断发展的领域之一。
另一个优化机会
上面的程序中还有另一个效率低下的地方:程序为x分配内存,将值7存储在该内存中,然后在随后的语句中访问内存以获得要打印的x(7)的值。由于x的值从不改变,因此这种内存访问是浪费的。
换句话说,在 as-if 规则下,编译器可以将上述程序优化为:
|
|
但为了进行这种优化,编译器必须确保x在定义和使用之间没有变化。由于x不是常量,编译器将不得不自己的分析来确定是否可进行这样的优化。虽然复杂的现代编译器可以做到这一点(至少在这个简单的情况下),但不是所有的编译器或在更复杂的情况下将无法进行这种优化。
与常量表达式优化(本质上是免费的)不同,这种优化可能无法自动进行。
然而,我们可以做少量的工作,帮助编译器,使其更有可能执行这种优化。
编译时常量
编译时常量是其值为常量表达式的常量。字面值(例如 1、2.3 和“Hello,world!”)是编译时常量的一种类型。
常变量可以是也可以不是编译时常量(取决于它们的初始化方式)。
编译时 const
如果常变量的初始值是常量表达式,则常变量是编译时常量。
考虑一个类似于上面的程序:
|
|
由于x和y的初始化值是常量表达式,因此x和y是编译时常量。这意味着x+y也是常量表达式。因此,当编译器编译该程序时,它可以计算x+y的值,并用结果7替换常量表达式。
请注意,编译时常量的初始值设定项可以是任何常量表达式。以下所有变量都是编译时常量变量:
|
|
命名常量 通常用作 编译时常变量:
|
|
编译时常量优化
编译时常量使编译器能够执行更多的优化。在许多情况下,优化完成后,编译时常变量的访问不会再出现在程序中。例如,每当使用编译时重力常数时,编译器可以简单地将标识符gravity替换为9.8,这避免了必须从内存中的某个位置获取值。
让我们回到前面的例子:
|
|
现在,让我们将x设置为编译时常量:
|
|
如果我们这样做,编译器就会知道x不会改变,并且很可能会将程序优化为:
|
|
关键点
使用编译时常量有助于编译器确定可以优化的内容。
运行时常量
如果常变量的初始值是非常量表达式,则是运行时常量。运行时常量是在运行时之前无法确定其初始化值的常量。
下面的示例说明了作为运行时常量的使用:
|
|
即使y有一个常量限定符,但初始值(getNumber() 的返回值)直到运行时才知道。因此,y是运行时常量,而不是编译时常量。由于y是运行时常量,因此必须在运行时计算z,表达式x+y也是运行时表达式。
关键点
编译时常量可以在常量表达式中使用,并允许更好的优化。运行时常量只能在非常量表达式中使用。它们的主要用途是确保对象的值不会被修改。
