在 C++20 标准中,constexpr 与 consteval 两个关键字都与常量表达式(constant expression)相关,但它们在使用时有着本质的不同。本文将通过示例和实战场景来阐明二者的区别、适用范围以及如何在模板编程中合理使用它们。
1. constexpr 简介
constexpr 表明一个函数或变量在编译期即可求值,满足“常量表达式”条件后仍可在运行时使用。它允许:
- 编译期求值:若调用时满足所有参数为常量,编译器会在编译阶段计算结果。
- 运行时可用:即使不满足编译期条件,也能在运行时使用,只是此时会在运行时计算。
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int a = square(5); // 编译期求值
int b = square(10); // 运行时求值
}
constexpr 适合用来实现可在编译期优化的数学函数、容器初始化等场景。
2. consteval 简介
consteval 是 C++20 新增的关键字,表示一定在编译期求值,否则编译错误。它是对 constexpr 的进一步限定,确保函数必须被调用为常量表达式。
consteval int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
constexpr int val = factorial(5); // 编译期求值
// int x = factorial(5); // ❌ 编译错误:必须在编译期求值
}
由于编译期必然执行,consteval 的函数往往在实现细节上可以更严格,例如不允许返回引用、使用非 constexpr 变量等。
3. 二者的区别
| 关键字 | 是否必须在编译期求值 | 可在运行时使用 | 适用场景 |
|---|---|---|---|
constexpr |
否 | 是 | 需要兼顾编译期优化与运行时灵活性 |
consteval |
是 | 否 | 只想在编译期执行、保证安全的函数 |
3.1 编译期求值的限制
constexpr的函数可以返回非常量值、使用if constexpr、递归等,只要在编译期满足所有条件即可。consteval的函数必须满足所有编译期要求,编译器会在调用点直接展开,若出现不可编译期求值的代码会报错。
3.2 语义上的提示
constexpr表示“尽可能在编译期”,但并不强制;consteval则是“绝对在编译期”。
4. 在模板编程中的应用
4.1 编译期生成数组
template<std::size_t N>
struct make_array {
static constexpr std::array<int, N> value = []{
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 <10>::value; // 编译期初始化
}
此时使用 constexpr,因为我们希望数组可以在运行时也使用。
4.2 编译期计算元数值
consteval std::size_t fib(std::size_t n) {
return n <= 1 ? n : fib(n-1) + fib(n-2);
}
template<std::size_t N>
struct fibonacci {
static constexpr std::size_t value = fib(N);
};
int main() {
static_assert(fibonacci <10>::value == 55);
}
fib 用 consteval,保证编译期递归展开;如果用 constexpr,static_assert 仍能通过,但如果有人把 fib 用于运行时调用,可能会产生不必要的运行时成本。
4.3 防止误用的 consteval
在一些库内部,你可能想确保某个算法只能在编译期使用,例如:
consteval int safe_divide(int a, int b) {
if (b == 0) throw "division by zero";
return a / b;
}
因为 consteval 强制编译期求值,任何错误都在编译阶段暴露,防止运行时错误。
5. 与 constinit 的关系
constinit 用于给全局/静态变量强制在编译期初始化,而不保证变量本身是常量。它经常与 constexpr 或 consteval 结合使用:
struct Config {
static constexpr int max_threads = 8;
};
constinit int global_threads = Config::max_threads; // 必须在编译期初始化
在这个例子中,global_threads 必须在编译期初始化,若 max_threads 不是 constexpr,会报错。
6. 结语
constexpr:灵活、兼容运行时,适合需要编译期优化但也可在运行时使用的场景。consteval:严格、强制编译期,适合保证安全性、消除运行时开销的函数。
在实际项目中,根据需求选择合适的关键字,既能得到编译期性能提升,又能保持代码的安全与可维护性。祝你在 C++20 的模板世界中玩得愉快!