C++ constexpr

在 C++ 中,constexpr 是 “constant expression(常量表达式)” 的缩写,用来声明可以在编译阶段直接算出结果的变量或函数。

可能有些小伙伴会问:“前面我们已经学过了 const,为什么 C++ 还要专门引入 constexpr 呢?”

其实这两者的侧重点并不一样。定义常量时使用的 const 主要表示 “只读”,它的初始值既可以在编译时确定,也可以等程序运行起来再计算。而 constexpr 的要求更严格,它明确规定结果必须在 “编译阶段” 就能算出来。

C++ constexpr 变量

所谓的 “常量表达式”,指的就是编译器在编译代码时就能直接算出结果的算式。像字面量 100、算式 4 * 8,或者由其他编译期常量组合而成的表达式,都属于常量表达式。

语法:

constexpr 数据类型 变量名 = 常量表达式;

说明:

声明 constexpr 变量时必须同时赋初值,而且等号右边必须是一个能在编译阶段算出结果的常量表达式。

因为它的值在生成可执行文件时就已经算好了,程序真正运行时直接拿现成的结果使用即可,还可以直接用来指定原生数组的长度。

示例 1:定义 constexpr 变量

#include <iostream>

int main() {
    constexpr int size = 5;
    constexpr int maxScore = 20 * size;
    int scores[size] = {88, 92, 76, 95, 90};

    std::cout << "size = " << size << '\n';
    std::cout << "maxScore = " << maxScore << '\n';
    std::cout << "末位成绩:" << scores[size - 1] << '\n';
    return 0;
}

运行结果如下。

size = 5
maxScore = 100
末位成绩:90

分析:

在这个例子中,我们定义了两个 constexpr 变量 size 和 maxScore。它们的初始值 5 和 20 * size 在编译阶段都能直接算出结果,因此 size 不仅自身不可修改,还能直接放进 scores[size] 的方括号里指定数组长度。

需要注意的是,普通变量的值在运行期间随时可能改变,不属于编译期常量。哪怕普通变量刚刚赋了初值,我们也不能拿它去初始化 constexpr 变量。下面这种写法就是错误的。

int n = 5;
constexpr int size = n;    // 编译报错

在这段代码中,n 是一个普通的运行期变量,编译器无法在编译阶段确定它的值,因此会直接报错。

平时写代码时,像数学常数或固定容量上限这类写代码时就能确定的值,我们优先使用 constexpr。如果一个值必须等程序运行后才能拿到,同时又不希望后续被修改,就使用 const。

C++ constexpr 函数

在 C++ 中,constexpr 不仅可以修饰变量,还可以用来修饰函数。我们在函数返回类型前面加上 constexpr,就表示这个函数支持在编译阶段直接计算返回值。

语法:

constexpr 返回类型 函数名(参数列表) {
    // 函数体
}

说明:

加上 constexpr 之后,这个函数是不是只能在编译阶段使用呢?其实并不是这样。一个 constexpr 函数可以同时兼顾 “编译期” 和 “运行期” 两种场景:

  • 编译期求值:当我们传入的实参都是编译期常量,并且返回值用于初始化 constexpr 变量或指定数组长度时,编译器会在编译阶段直接算出结果。
  • 运行期调用:当我们传入的实参包含普通的运行期变量时,它就会像普通函数一样,等到程序运行阶段再正常执行。

在我们目前使用的 C++20 标准下,constexpr 函数内部不仅可以写简单的算式,还可以正常定义局部变量、编写条件分支和循环语句。

示例 2:调用 constexpr 函数

#include <iostream>

constexpr int sumTo(int n) {
    int total = 0;
    for (int i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}

int main() {
    constexpr int s1 = sumTo(10);    // 编译期求值

    int limit = 100;
    int s2 = sumTo(limit);           // 运行期调用

    std::cout << "s1 = " << s1 << '\n';
    std::cout << "s2 = " << s2 << '\n';
    return 0;
}

运行结果如下。

s1 = 55
s2 = 5050

分析:

当我们第 1 次调用 sumTo(10) 时,传入的实参 10 是常量。由于返回值赋给了 constexpr 变量 s1,编译器在编译阶段就会跑完循环并算出结果 55。

当我们第 2 次调用 sumTo(limit) 时,由于 limit 是普通的运行期变量,程序会在运行阶段算出 5050 并赋给 s2。这里小伙伴们要注意,既然 sumTo(limit) 是在运行阶段才算出来的,就不能拿它去初始化 constexpr 变量。

int limit = 100;

// 正确方式
int s2 = sumTo(limit);

// 错误方式
constexpr int s3 = sumTo(limit);

在上面代码中,编译器在编译阶段不知道 limit 的具体数值,定义 s3 时就会直接报错。当然了,求 1 到 10 的累加和只是为了方便演示语法,在实际项目中,constexpr 函数常用来在编译期提前算好缓冲区大小、单位换算结果或固定查找表。

提示 因为 constexpr 函数随时需要在编译阶段求值,所以函数体内不能包含 std::cout 输出、std::cin 输入或文件读写这类必须在运行阶段才能执行的操作,否则将无法通过编译。

为什么需要 if constexpr?

在编写函数模板时,我们有时希望根据模板参数 T 的不同类型执行不同的逻辑。如果使用普通的 if...else 语句,编译器会把两个分支全都检查一遍。一旦某个分支对当前类型 T 不合法,哪怕程序运行时根本走不到那里,编译阶段也会直接报错。

为了解决这个问题,C++17 引入了 if constexpr 编译期条件分支。它会在编译阶段直接判断条件真假,只编译条件成立的那个分支,而把不成立的另一个分支直接丢弃。

语法:

if constexpr (编译期条件) {
    // 分支 1
} else {
    // 分支 2
}

说明:

if constexpr 小括号里的条件必须是编译阶段就能判断真假的常量表达式。在实际开发中,我们经常配合 <type_traits> 头文件提供的类型判断工具一起使用。

比如 std::is_integral_v<T> 就用来判断类型 T 是不是整数类型。其中 integral 表示 “整数”,末尾的 _v 是 “value(值)” 的缩写,合起来就能在编译期得出 true 或 false。

示例 3:按类型筛选编译分支

#include <iostream>
#include <string>
#include <type_traits>    // 提供 std::is_integral_v

template <typename T>
void processValue(T val) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "整数翻倍:" << val * 2 << '\n';
    } else {
        std::cout << "文本长度:" << val.size() << '\n';
    }
}

int main() {
    int num = 15;
    std::string text = "lvye";

    processValue(num);
    processValue(text);
    return 0;
}

运行结果如下。

整数翻倍:30
文本长度:4

分析:

从代码可以看到,整数 num 没有 size() 成员函数,字符串 text 也不能直接乘以 2。如果去掉 constexpr 改成普通的 if,编译器为 int 生成代码时依然会检查 else 分支里的 val.size(),结果就会编译报错。

加上 constexpr 之后,当我们传入整数 num 时,std::is_integral_v<int> 在编译期为 true,编译器会直接丢弃 else 分支。反过来传入 std::string 时,编译器则会丢弃第 1 个分支,只编译 val.size()。因此两次调用都能顺利跑通,小伙伴们可以自行去掉 constexpr 试一下报错效果。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号