constexpr 是 C++ 标准中用于标记可以在编译期求值的函数或变量的关键字。它最初在 C++11 中出现,目的是为了让编译器能够在编译阶段完成更多计算,从而减轻运行时负担。随着 C++ 发展,constexpr 的语义被逐步扩展,C++20 更是让它几乎与 const 互换。本文将从 C++11 的起点讲起,梳理各个标准版本中 constexpr 的演进,解析其背后的设计哲学,并给出实用的编码示例。
1. C++11:constexpr 的雏形
1.1 语法与限制
constexpr int square(int n) {
return n * n; // 只能是单个 return 语句
}
- 函数体只能包含一个
return语句,且不允许使用局部变量、循环、递归等。 - 不能出现非
constexpr的对象,不能使用动态内存分配。 - 变量声明必须使用
constexpr关键字,并且必须在编译期初始化。
1.2 应用场景
- 预计算表:例如
constexpr int table[5] = {1,4,9,16,25}; - 编译期大小:
sizeof(array) / sizeof(array[0]) - 让 STL 容器在编译期知道容量:
std::array<int, 10> arr;
2. C++14:宽松的 constexpr
C++14 对 constexpr 的限制大幅放宽,允许在函数体内出现循环、递归和多条语句。
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i)
result *= i;
return result;
}
2.1 递归支持
constexpr int fib(int n) {
return n < 2 ? n : fib(n-1) + fib(n-2);
}
编译器在编译期展开递归,得到 fib(10) = 55,并将其作为常量使用。
2.2 更灵活的变量
constexpr double pi = 3.14159265358979323846;
C++14 允许 constexpr 的类型可以是 double、float 等浮点类型,进一步提升了 constexpr 的实用性。
3. C++17:constexpr 的进一步提升
C++17 引入了 if constexpr,进一步让条件分支在编译期就能确定。
template <typename T>
constexpr T add(T a, T b) {
if constexpr (std::is_integral_v <T>) {
return a + b; // 整型加法
} else {
return a + b; // 浮点加法
}
}
3.1 结构化绑定与 constexpr
C++17 的结构化绑定也支持 constexpr,可以在编译期解构元组。
constexpr std::array<int,3> arr{1,2,3};
constexpr auto [x,y,z] = arr; // x=1, y=2, z=3
3.2 对容器的支持
在 C++17 之后,标准库的容器 std::array、std::vector 等都能与 constexpr 结合使用,只要满足编译期可构造。
constexpr std::array<int, 5> a = {1, 2, 3, 4, 5};
4. C++20:constexpr 的“真全能”
C++20 再次扩大了 constexpr 的范围,使得几乎所有标准库功能都可以在编译期使用。
4.1 允许 dynamic memory
constexpr std::vector <int> vec() {
std::vector <int> v;
for (int i=0; i<5; ++i) v.push_back(i);
return v;
}
以前 std::vector 的 push_back 需要运行时分配,现在编译器会在编译期完成这一步骤(如果满足 constexpr 的条件)。
4.2 consteval 与 constinit
consteval:强制函数必须在编译期求值。consteval int square(int n) { return n * n; }constinit:保证变量在编译期初始化,防止误用constexpr。
4.3 变长数组支持
C++20 引入了 std::span、std::bitset 等容器可以在 constexpr 上使用,进一步让编译期编程变得更自然。
5. 编译期 vs 运行期:实践经验
| 场景 | 选择 constexpr 还是 const |
|---|---|
| 需要在编译期生成常量 | constexpr |
| 只需要不可变 | const |
| 涉及复杂逻辑(循环、递归) | C++14+ constexpr |
| 需要使用 STL 容器 | C++20+ constexpr |
5.1 性能对比
- 编译期求值:编译器将结果直接写入可执行文件,减少运行时计算。
- 运行时求值:即使是
const,编译器也可能做优化,但无法保证完全消除计算。
5.2 调试与错误信息
使用 constexpr 的错误往往在编译期给出,信息较为直观;但若逻辑过于复杂,错误信息可能难以阅读。建议在编译期测试时使用 static_assert。
static_assert(square(5) == 25, "square error");
6. 代码示例:constexpr 与算法优化
下面给出一个典型的编译期求斐波那契数列前 30 个值的示例。
#include <array>
#include <iostream>
constexpr std::array<int, 30> build_fib() {
std::array<int, 30> arr = {0, 1};
for (size_t i = 2; i < 30; ++i) {
arr[i] = arr[i-1] + arr[i-2];
}
return arr;
}
int main() {
constexpr auto fib = build_fib();
for (int v : fib)
std::cout << v << ' ';
std::cout << '\n';
}
编译器在编译阶段完成整个数组的构造,fib 成为一个编译期常量。
7. 结语
从 C++11 的严格语义到 C++20 的几乎无限制,constexpr 已经成为 C++ 编程不可或缺的一部分。它让我们能够在编译期完成更复杂的计算,提升程序性能、减少运行时错误。理解其演进历程并掌握最佳实践,将帮助你在未来的 C++ 项目中写出更高效、更安全的代码。祝你在编译期编程的路上一路顺风!