在 C++ 17 之前,常常需要使用模板元编程或宏来实现编译期计算。自 C++ 11 引入 constexpr 之后,编译期计算变得更简单、更直观。本文将通过一个经典的斐波那契数列例子,演示如何使用 constexpr 进行编译期计算,并讨论常见的陷阱和优化技巧。
1. constexpr 的基本语法
constexpr int square(int x) {
return x * x;
}
constexpr标记函数、变量或类型在编译期可求值。- 函数体必须是常量表达式(即只能使用
constexpr函数、常量、原始数据类型、非虚函数等)。
2. 斐波那契数列的编译期实现
2.1 递归实现
constexpr int fib(int n) {
return n <= 1 ? n : fib(n-1) + fib(n-2);
}
- 这段代码在编译期会展开递归调用,最终得到一个常量。
- 但若
n很大,编译时间会显著增长。
2.2 迭代实现
constexpr int fib_iter(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
int tmp = a;
a = b;
b = tmp + b;
}
return a;
}
- 迭代方式在编译期同样可行,且对于大
n更节省编译时间。
3. constexpr 变量与对象
constexpr int FIB_10 = fib(10); // 55
constexpr int FIB_20 = fib_iter(20); // 6765
constexpr变量必须在定义时就能求值,否则会报错。
4. constexpr 与模板
template<int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial <0> {
static constexpr int value = 1;
};
- 通过模板递归同样可以在编译期计算阶乘。
5. 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 递归深度 | 编译器对 constexpr 递归深度有限制(通常 256 次)。若递归超出,可改为迭代。 |
| 类型限制 | 仅支持 POD 类型和已知值的 constexpr 函数。 |
| 非 constexpr 函数 | 函数体中若调用非 constexpr 函数,则整个函数失去 constexpr。 |
| 运行时条件 | 不能在 constexpr 中使用 if 或 while 依赖于运行时变量。 |
6. 性能与可读性
- 编译期计算将结果内联到二进制文件,运行时无需计算。
- 但过度使用会导致编译时间膨胀,甚至超出编译器默认限制。
- 适当的使用策略是:仅对确定且常量的值进行 constexpr 计算,避免在循环内部或大规模递归中使用。
7. 实践示例:在 std::array 中使用 constexpr
#include <array>
#include <iostream>
template<std::size_t N>
constexpr std::array<int, N> make_array() {
std::array<int, N> arr{};
for (std::size_t i = 0; i < N; ++i) {
arr[i] = static_cast <int>(i);
}
return arr;
}
int main() {
constexpr auto arr = make_array <5>();
for (int x : arr) std::cout << x << ' ';
std::cout << '\n';
}
- 这里
make_array在编译期生成固定大小的数组,运行时直接使用预生成的数据。
8. 结语
constexpr 的强大之处在于让编译器主动参与程序的计算,从而减轻运行时负担,提高程序效率。合理利用它,不仅可以让代码更简洁,也能让性能更上一层楼。下次在遇到需要预先计算的常量时,不妨尝试把它写成 constexpr,让编译器帮你完成这一步吧。