C++ constexpr变量详解:编译时常量完整指南

constexpr变量

Alex 2025年2月5日

在之前的第5.5课——常量表达式中,我们定义了什么是常量表达式,讨论了为什么常量表达式是可取的,并且以常量表达式何时实际在编译时求值作为结尾。

在本课中,我们将更仔细地研究如何在现代C++中创建可用于常量表达式的变量。我们还将探索确保代码实际在编译时执行的第一种方法。

编译时const挑战

在之前的课程中,我们提到,创建可用于常量表达式的变量的一种方法是使用const关键字。具有整型类型和常量表达式初始化器的const变量可用于常量表达式。所有其他const变量都不能用于常量表达式。

然而,使用const创建可用于常量表达式的变量存在一些挑战。

首先,使用const并不能立即清楚地表明变量是否可用于常量表达式。在某些情况下,我们可以比较容易地判断出来:

int a { 5 };       // 根本不是const
const int b { a }; // 显然不是常量表达式(因为初始化器是非const的)
const int c { 5 }; // 显然是常量表达式(因为初始化器是常量表达式)

在其他情况下,这可能相当困难:

const int d { someVar };    // 不清楚d是否可用于常量表达式
const int e { getValue() }; // 不清楚e是否可用于常量表达式

在上面的例子中,变量d和e是否可用于常量表达式取决于someVar和getValue()的定义。这意味着我们需要去检查这些初始化器的定义,并推断我们处于哪种情况。而且这可能还不够——如果someVar是const的,并且是用变量或函数调用初始化的,我们还得去检查它的初始化器的定义!

其次,使用const并没有提供一种方法来告知编译器我们要求一个可用于常量表达式的变量(并且如果它不是的话应该停止编译)。相反,它只会默默地创建一个只能用于运行时表达式的变量。

第三,使用const创建编译时常量变量并不扩展到非整型变量。而且有许多情况下我们希望非整型变量也是编译时常量。

constexpr关键字

幸运的是,我们可以借助编译器的力量来确保我们得到一个我们想要的编译时常量变量。为此,我们在变量声明中使用constexpr关键字(是"constant expression"的缩写)而不是const。constexpr变量总是编译时常量。因此,constexpr变量必须用常量表达式初始化,否则将导致编译错误。

例如:

#include <iostream>

// 非constexpr函数的返回值不是constexpr
int five()
{
    return 5;
}

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

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

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

    return 0;
}

由于函数通常在运行时执行,函数的返回值不是constexpr(即使返回表达式是常量表达式)。这就是为什么five()不是constexpr int f的合法初始化值。

相关内容

我们在F.1课——constexpr函数中讨论了返回值可以用于常量表达式的函数。

此外,constexpr也适用于非整型变量:

constexpr double d { 1.2 }; // d可用于常量表达式!

const与constexpr变量的含义

对于变量:

const表示对象的值在初始化后不能被改变。初始化器的值可能在编译时或运行时已知。const对象可以在运行时求值。 constexpr表示对象可用于常量表达式。初始化器的值必须在编译时已知。constexpr对象可以在运行时或编译时求值。 constexpr变量隐式地是const。const变量不是隐式的constexpr(除了具有常量表达式初始化器的const整型变量)。尽管变量可以被定义为同时具有constexpr和const,但在大多数情况下这是多余的,我们只需要使用const或constexpr中的一个。

与const不同,constexpr不是对象类型的一部分。因此,被定义为constexpr int的变量实际上具有const int类型(由于constexpr为对象提供的隐式const)。

最佳实践

任何具有常量表达式初始化器的常量变量都应该被声明为constexpr。

任何不具有常量表达式初始化器的常量变量(使其成为运行时常量)都应该被声明为const。

注意事项:在未来我们将讨论一些与constexpr不完全兼容的类型(包括std::string、std::vector以及其他使用动态内存分配的类型)。对于这些类型的常量对象,要么使用const而不是constexpr,要么选择一个与constexpr兼容的不同类型(例如std::string_view或std::array)。

术语

术语constexpr是"constant expression"的混合词。之所以选择这个名字,是因为constexpr对象(和函数)可以用于常量表达式。

正式地,constexpr关键字只适用于对象和函数。按照惯例,术语constexpr被用作任何常量表达式的缩写(例如1 + 2)。

作者注

这个网站上的一些例子是在采用constexpr的最佳实践之前写的——因此,你会注意到一些例子没有遵循上述最佳实践。我们目前正在更新不合规的例子,当我们遇到它们时。

对于高级读者

在C和C++中,声明一个数组对象(可以存储多个值的对象)需要在编译时知道数组的长度(它可以存储的值的数量),以便编译器可以确保为数组对象分配正确的内存量。

由于字面量在编译时已知,因此它们可以用作数组长度:

int arr[5]; // 一个包含5个int值的数组,长度5在编译时已知

在许多情况下,最好使用符号常量作为数组长度(例如,以避免魔法数字,并使数组长度在多个地方使用时更容易更改)。在C中,这可以通过预处理器宏或枚举器来实现,但不能通过const变量(不包括VLA,它有其他缺点)。C++希望改进这种情况,希望允许使用const变量而不是宏。但是,变量的值通常被认为只在运行时已知,这使得它们不能用作数组长度。

为了解决这个问题,C++语言标准增加了一个豁免,即具有常量表达式初始化器的const整型类型将被视为在编译时已知的值,因此可以用作数组长度:

const int arrLen = 5;
int arr[arrLen]; // 好的:一个包含5个int的数组

当C++11引入常量表达式时,让具有常量表达式初始化器的const int被纳入该定义是合理的。委员会讨论了是否也应该包括其他类型,但最终决定不包括。

const和constexpr函数参数

普通的函数调用在运行时求值,提供的参数用于初始化函数的参数。由于函数参数的初始化发生在运行时,这导致了两个后果:

const函数参数被视为运行时常量(即使提供的参数是编译时常量)。 函数参数不能被声明为constexpr,因为它们的初始化值直到运行时才确定。

相关内容

我们在下面讨论可以在常量表达式中调用的函数(因此可以用于常量表达式)。

C++还支持一种将编译时常量传递给函数的方法。我们在11.9课——非类型模板参数中讨论这些内容。

术语回顾

术语 定义 编译时常量 其值必须在编译时已知的值或不可修改对象(例如字面量和constexpr变量)。 constexpr 声明对象为编译时常量(和可以编译时求值的函数)的关键字。非正式地,常量表达式的缩写。 常量表达式 只包含编译时常量和编译时可求值的运算符/函数的表达式。 运行时表达式 不是常量表达式的表达式。 运行时常量 不是编译时常量的值或不可修改对象。

constexpr函数简介

constexpr函数是一个可以在常量表达式中调用的函数。当constexpr函数所在的常量表达式必须在编译时求值时(例如,在constexpr变量的初始化器中),constexpr函数必须在编译时求值。否则,constexpr函数可以在编译时(如果符合条件)或运行时求值。要符合编译时执行的条件,所有参数必须是常量表达式。

要创建一个constexpr函数,将constexpr关键字放在函数声明的返回类型之前:

#include <iostream>

int max(int x, int y) // 这是一个非constexpr函数
{
    if (x > y)
        return x;
    else
        return y;
}

constexpr int cmax(int x, int y) // 这是一个constexpr函数
{
    if (x > y)
        return x;
    else
        return y;
}

int main()
{
    int m1 { max(5, 6) };            // 好的
    const int m2 { max(5, 6) };      // 好的
    constexpr int m3 { max(5, 6) };  // 编译错误:max(5, 6)不是常量表达式

    int m4 { cmax(5, 6) };           // 好的:可以在编译时或运行时求值
    const int m5 { cmax(5, 6) };     // 好的:可以在编译时或运行时求值
    constexpr int m6 { cmax(5, 6) }; // 好的:必须在编译时求值

    return 0;
}

作者注

我们以前在本章中详细讨论了constexpr函数,但读者的反馈表明,这个主题太长且微妙,不适合在教程系列的早期介绍。因此,我们将完整讨论移回F.1课——constexpr函数。

从这个介绍中要记住的关键是,constexpr函数可以在常量表达式中调用。

你将在一些未来的例子中看到constexpr函数的使用(在适当的情况下),但在我们正式介绍这个主题之前,我们不会期望你进一步理解它们或编写自己的constexpr函数。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"