在 C++17 之后,编译期计算的能力已经被大大提升。constexpr 关键字让我们可以把函数、对象甚至整个类声明为“常量表达式”,它们可以在编译期求值,进而被用作模板参数、数组尺寸、枚举值等需要编译期常量的地方。C++20 再次对这一功能做了加强,引入了 consteval 和更灵活的 constexpr if。下面,我们将从理论到实践,系统地剖析这两种机制,并给出实用的编码技巧。
一、constexpr 的演进
| 版本 |
关键特性 |
说明 |
| C++11 |
只能声明简单的 constexpr 函数(不含循环、递归等) |
受限的语法,易产生“constexpr 失败” |
| C++14 |
允许 constexpr 内部出现循环、递归、异常捕获 |
逐步接近普通函数 |
| C++17 |
通过 if constexpr 支持编译期条件判断 |
可在模板中实现更灵活的分支 |
| C++20 |
引入 consteval,强制在编译期求值;允许 constexpr 内部使用更丰富的语法(如 new、try/catch 等) |
进一步提升安全性与可读性 |
1.1 constexpr 函数的典型写法
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
在 C++14 之前,这种递归写法会报错。C++14 起,编译器会把 factorial(5) 在编译期求值,最终得到 120。
1.2 constexpr 对象和类
struct Point {
int x, y;
constexpr Point(int a, int b) : x(a), y(b) {}
constexpr int dist2() const { return x * x + y * y; }
};
constexpr Point p{3, 4};
static_assert(p.dist2() == 25, "距离错误");
通过 constexpr 对象,我们可以在 static_assert 或数组尺寸中使用。
二、constexpr if:编译期分支
constexpr if 允许在模板内部根据类型或常量值在编译期选择执行哪条语句块。相比传统的 std::enable_if 或 if constexpr 之前的实现,它更易读、更直观。
2.1 基础用法
template <typename T>
void print_type_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";
}
}
编译器在实例化 print_type_info 时,只会编译匹配的分支,未匹配分支会被剔除,避免了无效代码编译错误。
2.2 与 constexpr 函数结合
constexpr int safe_divide(int a, int b) {
if constexpr (b == 0) {
// 通过编译期错误提示
static_assert(b != 0, "除数不能为零");
return 0; // unreachable
} else {
return a / b;
}
}
此处 static_assert 只在 b == 0 时触发。
2.3 在类模板中使用
template <typename T>
struct Serializer {
void serialize(const T& obj) {
if constexpr (std::is_same_v<T, std::string>) {
// string 序列化
} else if constexpr (std::is_arithmetic_v <T>) {
// 数值序列化
} else {
static_assert(always_false <T>::value, "Unsupported type");
}
}
};
这样可以在编译期就知道该类型不支持序列化,给出清晰的错误信息。
三、实践技巧
3.1 避免常见陷阱
-
忘记 constexpr 函数的返回值
constexpr 函数不一定在编译期被求值。只有当它作为 constexpr 上下文(如模板参数、static_assert 等)使用时才会被求值。
constexpr int add(int a, int b) { return a + b; }
int x = add(1, 2); // 这里在运行时计算
static_assert(add(1, 2) == 3, "编译期检查");
-
递归深度限制
编译器对递归展开深度有限制(通常 1024 次)。若需要更深层次递归,考虑使用迭代或 std::integral_constant 组合。
-
使用 consteval 取代 constexpr
当你想强制某个函数只能在编译期调用时,使用 consteval,否则会得到编译错误,而不是运行时执行。
consteval int square(int n) { return n * n; }
constexpr int a = square(5); // OK
int b = square(5); // 编译错误
3.2 性能考量
虽然编译期计算可以减少运行时负担,但过度使用可能导致编译时间显著增长。常见优化策略:
- 将不需要编译期结果的代码保持为普通函数。
- 对于大数组或复杂表格,考虑在构建阶段使用
constexpr 初始化,然后写入文件或使用生成器。
- 对于运行时决定的分支,保留
if constexpr,仅在类型/值可确定时使用。
四、案例:实现一个编译期哈希表
下面给出一个极简的编译期哈希表实现,演示 constexpr、constexpr if 与模板元编程的协作。
#include <array>
#include <cstddef>
#include <string_view>
constexpr std::size_t fnv1a_hash(const char* s, std::size_t h = 14695981039346656037ULL) {
return *s ? fnv1a_hash(s + 1, (h ^ static_cast<std::size_t>(*s)) * 1099511628211ULL) : h;
}
template <std::size_t N>
struct ConstMap {
struct Entry {
std::size_t key;
const char* value;
};
std::array<Entry, N> data{};
constexpr ConstMap(const std::array<std::pair<std::string_view, const char*>, N>& init) {
std::size_t i = 0;
for (auto& p : init) {
data[i++] = { fnv1a_hash(p.first.data()), p.second };
}
}
constexpr const char* get(std::string_view key) const {
std::size_t h = fnv1a_hash(key.data());
for (const auto& e : data) {
if (e.key == h) return e.value;
}
return nullptr;
}
};
constexpr std::array<std::pair<std::string_view, const char*>, 3> init{
std::pair{"one", "1"},
std::pair{"two", "2"},
std::pair{"three", "3"}
};
constexpr ConstMap <3> cmap(init);
static_assert(cmap.get("two") != nullptr, "键未找到");
static_assert(cmap.get("two")[0] == '2', "值错误");
fnv1a_hash 在编译期计算字符串哈希。
ConstMap 用 constexpr 构造器把键值对初始化到数组中。
get 方法在编译期遍历数组查找匹配哈希值。
static_assert 验证了映射正确。
此实现适用于需要在编译期完成配置表、错误码映射等场景,避免运行时初始化开销。
五、总结
constexpr 让我们能够把函数、对象、甚至类声明为编译期常量,从而提升程序安全性和性能。
constexpr if 进一步增强了模板的条件分支能力,使编译期逻辑更简洁、错误更易定位。
- C++20 引入
consteval 与更丰富的 constexpr 内部语法,为编译期计算提供更强大工具。
- 在实际编码中,需要权衡编译期计算与编译时间成本,合理使用上述特性。
掌握这些技巧后,你将能够写出更安全、更高效、且在编译期完成更多预处理任务的 C++ 代码。祝你在编译期计算的世界里玩得愉快!