constexpr 关键字是 C++11 引入的,随后在 C++14、C++17、C++20 以及 C++23 中不断增强。它的核心理念是:如果能在编译期完成某个计算,就尽量让编译器去做,以减轻运行时负担、提升安全性、增强可验证性。本文将从 constexpr 的历史变迁、语法细节、常见使用场景、以及与现代 C++ 技术的结合展开探讨,帮助你在实际项目中高效利用这一特性。
1. constexpr 的演进历程
| 标准 | 主要改动 |
|---|---|
| C++11 | 引入 constexpr,只能是 constexpr 构造函数、变量或函数,且函数体必须是单个 return 语句 |
| C++14 | 允许 constexpr 函数体中出现循环、条件判断、递归调用等 |
| C++17 | constexpr 函数可以返回非 constexpr 类型(如 std::vector),支持 constexpr 初始化 std::string_view 等 |
| C++20 | 引入 consteval,强制编译期求值;支持 if constexpr、switch constexpr;constexpr 变量可以是类类型并拥有非平凡构造函数 |
| C++23 | 进一步提升 constexpr 的能力,允许 constexpr 变量初始化包含动态内存分配(在满足特定条件下) |
你可能会注意到 C++20 的
consteval与constexpr的区别:前者要求在任何调用处都必须在编译期求值,而后者允许在运行时求值。两者共同为编译期编程提供了更细粒度的控制。
2. constexpr 的语法要点
2.1 变量
constexpr int factorial_5 = []{
int r = 1;
for (int i = 2; i <= 5; ++i) r *= i;
return r;
}(); // 结果为 120
- 注意:
constexpr变量必须在定义时完成初始化。对于类成员,需要在constexpr构造函数中初始化。
2.2 函数
constexpr int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b); // 递归
}
- 编译期调用:
static_assert(gcd(48, 18) == 6, "GCD error"); - 运行时调用:
int r = gcd(48, 18);
2.3 类
struct Point {
int x, y;
constexpr Point(int x_, int y_) : x(x_), y(y_) {}
constexpr int dist2(const Point& other) const {
int dx = x - other.x;
int dy = y - other.y;
return dx * dx + dy * dy;
}
};
constexpr Point p1(3, 4), p2(0, 0);
static_assert(p1.dist2(p2) == 25, "Distance error");
- 从 C++20 起,
constexpr类可以拥有mutable成员,但只能在constexpr成员函数中修改。
3. 常见使用场景
3.1 常量表达式计算
constexpr int prime_mask = []{
int mask = 0;
for (int i = 2; i < 31; ++i) {
bool is_prime = true;
for (int j = 2; j * j <= i; ++j) {
if (i % j == 0) { is_prime = false; break; }
}
if (is_prime) mask |= (1 << i);
}
return mask;
}();
- 用于位掩码、快速判断素数等。
3.2 编译期数据结构
template<std::size_t N>
constexpr std::array<int, N> init_array() {
std::array<int, N> a{};
for (std::size_t i = 0; i < N; ++i) a[i] = static_cast<int>(i * i);
return a;
}
constexpr auto squares = init_array <10>();
- 生成编译期
std::array,可直接用于constexpr逻辑。
3.3 预编译模板元编程
template<typename T>
struct is_integral_v : std::integral_constant<bool,
std::is_same_v<T, int> || std::is_same_v<T, long> /* ... */> {};
template<typename T>
constexpr bool is_supported = is_integral_v <T>::value;
constexpr结合 SFINAE、if constexpr可以在编译期决定模板路径。
3.4 性能优化
- 编译期初始化:减少运行时构造成本,尤其在嵌入式系统或高性能计算中尤为重要。
- constexpr 运算:避免在循环中多次重复计算,如
constexpr版本的斐波那契数列。
4. 与其他现代 C++ 技术的结合
| 技术 | 说明 |
|---|---|
if constexpr |
在编译期决定分支,避免运行时条件检查。 |
| 模板参数化常量 | template<int N> 使得 constexpr 值可用于数组大小。 |
std::bitset、std::array |
允许 constexpr 初始化,提升表达式可读性。 |
consteval |
对于必须在编译期计算的函数,可强制使用。 |
constexpr 与 nodiscard |
结合提示错误,例如 constexpr int factorial(-1) [[nodiscard]] 可在编译期警告。 |
示例:使用 if constexpr 进行类型特化
template<typename T>
constexpr void print_info(const T& value) {
if constexpr (std::is_integral_v <T>) {
std::cout << "Integral: " << value << '\n';
} else if constexpr (std::is_floating_point_v <T>) {
std::cout << "Floating: " << value << '\n';
} else {
std::cout << "Other type\n";
}
}
此函数在编译期即可决定调用哪条分支,运行时无额外条件判断。
5. 实战案例:编译期生成查找表
在 DSP 或加密算法中,经常需要大量查找表。我们可以利用 constexpr 生成:
constexpr std::array<double, 256> sin_table() {
std::array<double, 256> arr{};
for (int i = 0; i < 256; ++i)
arr[i] = std::sin(2 * M_PI * i / 256);
return arr;
}
constexpr auto sine = sin_table();
double fast_sin(double x) {
int index = static_cast <int>(x / (2 * M_PI) * 256) & 255;
return sine[index];
}
- 生成表时已在编译期完成,无需运行时初始化。
fast_sin只需一次整数运算即可得到近似结果。
6. 常见陷阱与注意事项
- 递归深度:
constexpr递归函数在编译期递归深度有限(取决于编译器实现,通常 1024 次)。若递归深度超过,需改用循环或迭代。 - 异常抛出:C++23 允许
constexpr函数抛异常,但仍需在编译期不触发。若不确定,避免在constexpr函数中使用throw。 - 动态内存:从 C++20 起,
constexpr允许new,但仅在满足编译期分配条件时才会成功。若不满足,编译器会报错。 - 模板实例化:
constexpr变量在不同翻译单元中会被多次实例化,若体积较大可能导致编译时间增长。可考虑使用inline或constexpr inline。
7. 结语
constexpr 已从一个简单的“编译期常量”演进为现代 C++ 编译期编程的核心工具。它让你能够在编译阶段完成复杂运算、生成数据结构,并与模板元编程无缝协作,从而提升程序的性能、可维护性和可验证性。希望本文能帮助你在项目中更好地利用 constexpr,让代码更高效、更安全。祝编码愉快!