在 C++20 之前,constexpr 函数的能力已经大幅提升,但它们仍然有一些严格的限制。随着 C++20 的发布,constexpr 函数获得了更高的灵活性和性能优势,成为编译期和运行期计算的桥梁。本文将深入探讨 C++20 constexpr 函数的新特性、常见用例以及最佳实践,帮助你充分利用这一强大工具。
1. constexpr 的演进
1.1 早期 constexpr(C++11–C++14)
- C++11:只能包含返回字面量的表达式、单一
return语句、对全局变量的读写有限制。 - C++14:允许循环、
if语句、异常处理,但仍不支持全局写、new/delete、静态变量等。
1.2 C++17 里程碑
- 支持
try/catch、constexpr变量初始化的更复杂表达式。 - 允许在
constexpr函数内部声明static变量,但只能读写。
1.3 C++20 的大步跨越
- 完全可变:可以在 constexpr 函数中执行
new/delete,操作动态内存。 - 静态变量:
static变量可以被修改,且在多次调用中保持状态。 - 协程:
constexpr函数可以与co_yield、co_return协同工作。 - 模板参数推断:更强大的模板元编程支持。
2. 典型用例
2.1 预计算多项式系数
constexpr double poly(double x) {
return ((3.0 * x + 2.0) * x - 5.0) * x + 1.0;
}
int main() {
constexpr double result = poly(2.5);
static_assert(result == 23.375, "Unexpected value");
}
这里,poly 在编译期计算,减少运行时负担。
2.2 生成运行时可变状态的对象
constexpr std::string_view make_prefix() {
static std::string prefix = "Log: ";
return prefix;
}
static std::string prefix 可以在 constexpr 函数中被修改,例如在程序启动阶段动态构造日志前缀。
2.3 constexpr 协程生成序列
#include <coroutine>
#include <iostream>
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T value_;
Generator get_return_object() { return {handle_type::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
template<typename U>
std::suspend_always yield_value(U&& v) {
value_ = std::forward <U>(v);
return {};
}
void return_void() {}
};
handle_type handle_;
explicit Generator(handle_type h) : handle_(h) {}
~Generator() { handle_.destroy(); }
T next() { handle_.resume(); return handle_.promise().value_; }
};
constexpr Generator <int> range(int start, int end) {
for (int i = start; i <= end; ++i)
co_yield i;
}
在 C++20 编译期内,你可以使用此协程生成固定范围的整数序列,既不需要动态内存也能保证类型安全。
3. 性能评估
3.1 编译期 vs 运行期
- 编译期计算:
constexpr的主要优势是将复杂计算移到编译阶段,减少运行时 CPU 周期。适用于配置数据、数学常数表、预计算路径等。 - 运行期动态:在
constexpr函数中使用new/delete时,若不在编译期可确定对象大小,编译器可能会产生运行时开销。但在需要动态内存但仍想保持 constexpr 语义时,它提供了灵活性。
3.2 典型基准
- 例子:在一个包含 10,000 个点的三角形剖分算法中,使用
constexpr预生成顶点坐标表可将 CPU 时间从 150ms 降到 10ms(编译期计算占用 ~5ms,但显著减少了运行时循环)。
4. 常见陷阱
- 循环计数不确定:如果循环的迭代次数无法在编译期确定,编译器将把它放到运行时。
- 递归深度:递归 constexpr 函数在编译期可能导致堆栈溢出,需限制递归深度或使用迭代方式。
- 异常抛出:虽然 C++20 允许在 constexpr 中使用
try/catch,但抛出异常会导致编译失败,除非异常在 constexpr 评估中被捕获并处理。
5. 最佳实践
| 场景 | 推荐做法 |
|---|---|
| 需要预计算常量 | 直接使用 constexpr 表达式或函数 |
| 需要动态内存但保持 constexpr 语义 | 在 C++20 内使用 new/delete,但注意对象生命周期 |
| 需要协程式生成序列 | 使用 co_yield 与 Generator 结构体 |
| 大量递归 | 尽量改为迭代或使用模板元编程 |
6. 结语
C++20 的 constexpr 函数已从“只能在编译期做有限计算”演进为“一种能够在编译期与运行期之间无缝切换的强大工具”。掌握其新特性并合理运用,可以显著提升程序性能、可维护性和表达力。无论你是嵌入式开发、游戏编程还是高性能计算,理解并使用 C++20 constexpr 将为你的项目带来新的可能性。