在 C++17 之前,constexpr 仅能限定函数体内的返回值必须是常量,并且函数体不能包含循环、条件语句等复杂控制流。而 C++17 允许在 constexpr 函数中使用 if、switch、循环等语句,极大地提升了在编译期执行复杂算法的能力。
与模板元编程结合后,constexpr 成为一种极具表现力的工具:我们可以在编译期完成数值计算、类型判定、数据结构构建等,从而在运行时获得更高的性能和更安全的代码。
1. 经典例子:斐波那契数列
先看 C++11 的实现:
constexpr unsigned long long fib(unsigned int n) {
return n < 2 ? n : fib(n-1) + fib(n-2);
}
这段代码在编译期会递归展开,计算出常量 fib(20) 等。
然而如果我们想在编译期生成一个斐波那契数列数组,并将其作为 constexpr 对象使用,C++11 的 constexpr 函数已无法做到。
C++17 版本:
#include <array>
#include <cstddef>
template<std::size_t N>
constexpr std::array<unsigned long long, N> make_fib() {
std::array<unsigned long long, N> arr{};
for (std::size_t i = 0; i < N; ++i) {
if (i < 2) {
arr[i] = i;
} else {
arr[i] = arr[i-1] + arr[i-2];
}
}
return arr;
}
constexpr auto fib_array = make_fib <20>();
此时 fib_array 在编译期就完成了填充,程序运行时可以直接使用,而不需要任何运行时计算。
2. 与模板元编程的结合
模板元编程(TMP)往往通过递归模板实例化来实现编译期计算,例如:
template<int N>
struct factorial {
static constexpr int value = N * factorial<N-1>::value;
};
template<>
struct factorial <0> { static constexpr int value = 1; };
C++17 的 constexpr 可以直接取代部分递归模板,使代码更简洁。
例如,计算斐波那契数列的第 N 项可以写成:
constexpr unsigned long long fib_calc(unsigned int n) {
unsigned long long a = 0, b = 1;
for (unsigned int i = 0; i < n; ++i) {
unsigned long long tmp = a + b;
a = b;
b = tmp;
}
return a;
}
然后在模板中使用:
template<int N>
struct fib_meta {
static constexpr unsigned long long value = fib_calc(N);
};
这样既保留了模板的类型层级逻辑,又利用 constexpr 让数值计算真正发生在编译期。
3. 现代编译期优化:if constexpr
C++17 还引入了 if constexpr,这是一种在编译期决定分支的语句。结合模板,可以在同一函数中根据类型参数执行不同逻辑,而编译器会在编译期剔除不匹配的分支,避免产生不合法代码。
template<typename T>
constexpr T square(T x) {
if constexpr (std::is_integral_v <T>) {
return x * x; // 整型平方
} else {
return std::pow(x, 2); // 非整型使用 std::pow
}
}
4. 典型应用场景
- 编译期常量表达式:在头文件中使用
constexpr生成大型常量表,如三角函数表、正弦表等。 - 类型安全的字符串处理:利用
constexpr计算字符串长度、拼接等,避免运行时分配。 - 编译期哈希表:将字符串常量映射为哈希值,构建编译期查找表。
- 模板库的元编程优化:将复杂的递归模板逻辑替换为
constexpr函数,提升编译速度。
5. 小结
C++17 对 constexpr 的扩展大大增强了在编译期执行逻辑的能力,使得模板元编程与运行时性能优化得以更紧密地结合。通过合理利用 constexpr、if constexpr 与模板特化,可以编写出既高效又可读性更好的现代 C++ 代码。