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::vector 和 std::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::sort、std::find、std::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 改进的核心脉络,我建议你保存下来对照学习。
实际应用场景
说了这么多理论,来点实际的。我在做嵌入式开发时,经常需要预计算一些数学表——比如正弦表、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 最值得学习的特性之一。