29. C++20 改进的 constexpr:编译期计算的全面进化

说实话,C++11 刚引入 constexpr 那会儿,我觉得它挺鸡肋的。只能写一个 return 语句,能做的事太有限了。到了 C++14 放宽了不少,C++17 又加了 if constexpr。但真正让我觉得「哇,这玩意儿终于能用了」的,是 C++20 这一波改进。

C++20 对 constexpr 的增强,说白了就是让编译期计算的能力大幅扩展。以前只能在编译期算个阶乘、斐波那契数列,现在连 vector、string、动态分配都能在编译期搞定了。我个人习惯把这一系列改进称为「constexpr 的成年礼」。

constexpr 容器的到来

先说说最让我兴奋的部分——constexpr 容器。C++20 标准库中,std::vectorstd::string 的很多操作都被标记为 constexpr 了。这意味着你可以在编译期创建、修改、遍历这些容器。

我在项目中遇到过这样一个场景:需要根据配置文件生成一组固定的查找表。以前的做法是写个脚本在构建时生成代码,或者用模板元编程硬编码。现在好了,直接在 constexpr 函数里用 vector 搞定。

#include <vector>
#include <algorithm>

constexpr std::vector<int> generate_lookup_table() {
    std::vector<int> vec;
    vec.reserve(100);
    for (int i = 0; i < 100; ++i) {
        vec.push_back(i * i);
    }
    // 排序一下
    std::sort(vec.begin(), vec.end(), std::greater<>());
    return vec;
}

// 编译期就计算好了
constexpr auto table = generate_lookup_table();

int main() {
    // table[0] 在编译期就是 9801
    static_assert(table[0] == 99 * 99);
    return 0;
}

关键点:constexpr 容器在编译期创建,但生命周期仅限于编译期。你不能把 constexpr vector 的引用传递到运行时——嗯,这其实是个合理的限制。

constexpr 算法:编译期也能用 STL 算法了

C++20 之前,想在编译期对数组排序?你得自己手写一个 constexpr 排序函数。现在不用了,std::sortstd::findstd::copy 等大量算法都支持 constexpr 了。

你想想看,这意味着什么?以前需要在运行时做的数据处理,现在可以提前到编译期完成。程序启动速度更快,运行时开销更小。

#include <algorithm>
#include <array>

constexpr std::array<int, 5> process_data() {
    std::array<int, 5> data = {5, 3, 1, 4, 2};
    std::sort(data.begin(), data.end());
    // 去重
    auto last = std::unique(data.begin(), data.end());
    // 注意:这里 last 是迭代器,但 array 大小固定
    // 实际项目中可以用 vector 更灵活
    return data;
}

constexpr auto result = process_data();
// result = {1, 2, 3, 4, 5}

避坑指南:我曾经在 constexpr 算法上栽过跟头——不是所有算法都支持 constexpr。比如 std::shuffle 依赖随机数生成器,目前还不是 constexpr 的。用之前最好查一下 cppreference 确认。

constexpr 动态分配:编译期 new 和 delete

这是 C++20 最炸裂的改进之一。以前 constexpr 函数里不能动态分配内存,现在可以了!但有个重要限制:编译期分配的内存必须在编译期释放完。说白了,不能有内存泄漏。

constexpr int dynamic_example() {
    int* p = new int(42);
    int result = *p;
    delete p;  // 必须释放,否则编译错误
    return result;
}

constexpr int val = dynamic_example();
// val = 42

这个特性让 constexpr 容器的实现成为可能。vector 内部就是用动态分配来管理存储的。编译期分配的内存会被编译器跟踪,确保在 constexpr 求值结束时全部释放。

注意:constexpr 动态分配有「瞬态分配」的概念。分配的内存在 constexpr 求值过程中创建并销毁,不能泄漏到运行时。如果你试图返回一个指向编译期分配内存的指针,编译器会报错。

编译期字符串处理

结合 constexpr string 和算法,编译期字符串处理变得非常强大。我以前写过一个编译期 URL 解析器,用来在编译期提取配置信息。

#include <string>
#include <string_view>

constexpr std::string extract_domain(const std::string& url) {
    auto pos = url.find("://");
    if (pos == std::string::npos) return "";
    auto start = pos + 3;
    auto end = url.find('/', start);
    if (end == std::string::npos) end = url.size();
    return url.substr(start, end - start);
}

constexpr auto domain = extract_domain("https://www.example.com/path");
// domain = "www.example.com"

知识体系总览

下面这张图总结了 C++20 constexpr 改进的核心脉络,我建议你保存下来对照学习。

C++20 constexpr 改进全景图 constexpr 增强 constexpr 容器 std::vector std::string 编译期创建/修改 constexpr 算法 std::sort / std::find std::copy / std::unique 编译期数据处理 动态分配 编译期 new/delete 瞬态分配 无泄漏要求 编译期字符串 查找/拼接/分割 编译期解析

实际应用场景

说了这么多理论,来点实际的。我在做嵌入式开发时,经常需要预计算一些数学表——比如正弦表、CRC 校验表。以前要么用 constexpr 手写循环,要么用外部工具生成。现在直接上 vector 加算法,代码清晰多了。

#include <vector>
#include <cmath>  // 注意:cmath 不是 constexpr,这里只是示意

// 编译期生成正弦查找表
constexpr std::vector<double> generate_sin_table(int steps) {
    std::vector<double> table;
    table.reserve(steps);
    for (int i = 0; i < steps; ++i) {
        // 实际项目中可以用 constexpr 数学库
        table.push_back(0.0);  // 占位
    }
    return table;
}

constexpr auto sin_table = generate_sin_table(360);

个人经验:我建议你在使用 constexpr 容器时,注意编译时间和内存消耗。编译期计算虽然快,但太复杂的 constexpr 函数会让编译器「想很久」。如果遇到编译超时,可以考虑把计算拆分成多个小函数。

限制与注意事项

特性 支持情况 注意事项
constexpr vector ✅ 完全支持 不能泄漏到运行时
constexpr string ✅ 完全支持 注意字符串长度
constexpr 算法 ✅ 大部分支持 随机算法不支持
动态分配 ✅ 支持 必须编译期释放
虚函数 ❌ 不支持 C++20 未包含
try/catch ❌ 不支持 异常处理不可用

嗯,这里要特别提醒一下:constexpr 动态分配虽然强大,但滥用会导致编译期内存暴涨。我曾经见过一个同事在 constexpr 函数里递归创建大量 vector,结果编译器直接爆内存了。合理控制数据规模很重要。

总的来说,C++20 的 constexpr 改进让编译期计算真正变得实用。以前需要模板元编程或者外部工具才能做的事,现在用普通的 C++ 代码就能在编译期完成。我个人觉得,这是 C++20 最值得学习的特性之一。


公众号:蓝海资料掘金营,微信deep3321