C++ 中的 constexpr 与 consteval:什么时候用哪个?

在 C++20 之前,constexpr 是实现编译期计算的主要手段。随着 C++20 的到来,consteval 作为一种新的关键字被引入,用于强制表达式在编译期求值。两者看似相似,却有不同的语义、适用场景和限制。下面从定义、用途、编译期行为以及实际使用的最佳实践四个角度对它们进行对比,并给出实际示例,帮助你在项目中做出更合适的选择。

1. 基本定义

关键字 说明 编译期求值
constexpr 声明函数或变量可以在编译期求值,若满足约束则可以在编译期执行 可选,满足条件时可在编译期求值
consteval 声明函数或变量必须在编译期求值 必须在编译期求值,编译器会报错如果无法在编译期完成

注意constexpr 不是 consteval 的子集。consteval 要求 强制 编译期求值,而 constexpr 则允许在需要时退回到运行时。

2. 典型使用场景

constexpr

  • 常量表达式:在数组大小、模板参数等处需要编译期常量。
  • 延迟求值:允许函数在运行时被调用,若在编译期可以求值则进行求值。
  • 跨 C++ 标准兼容:在 C++17 及之前版本可用,兼容更广泛的编译器。

consteval

  • 编译期错误检查:当某些错误只能在编译期发现时,使用 consteval 可确保程序在编译阶段即报错。
  • 强制生成静态断言:例如 consteval int factorial(int n) { static_assert(n <= 10, "Too large"); ... }
  • 模板元编程的前置验证:对模板参数做严格检查,防止无效实例化。

3. 语义细节与限制

  1. 函数返回类型

    • constexpr 函数的返回值可以是普通类型、引用或 const
    • consteval 函数的返回值不允许是 auto 推导为引用类型,必须是可复制或移动的类型。
  2. 递归

    • constexpr 函数可以递归,但编译器对递归深度有限制。
    • consteval 函数也可以递归,但编译器在编译期会进行完整展开,深度限制更严格。
  3. 异常处理

    • constexpr 函数可以抛异常,只要在编译期调用时不会抛出。
    • consteval 函数不能抛异常,因为所有调用都必须在编译期完成,且编译器不支持在编译期抛异常。
  4. 可见性

    • constexpr 可以在运行时使用;consteval 只能在编译期调用。
    • 任何试图在运行时调用 consteval 函数都会导致编译错误。

4. 代码示例

#include <iostream>
#include <array>
#include <concepts>

/* 1. constexpr 示例:编译期数组大小 */
constexpr std::size_t fibonacci_n(std::size_t n) {
    return (n < 2) ? n : fibonacci_n(n - 1) + fibonacci_n(n - 2);
}

constexpr std::size_t N = fibonacci_n(10);
std::array<int, N> fibs; // 编译期确定大小

/* 2. consteval 示例:强制编译期检查 */
consteval int square_root(int x) {
    if (x < 0) throw "负数"; // 编译期抛异常,导致编译错误
    return static_cast <int>(std::sqrt(x));
}

int main() {
    // constexpr 可以在运行时使用
    constexpr int a = fibonacci_n(5);
    std::cout << "fibonacci(5) = " << a << '\n';

    // consteval 必须在编译期调用
    constexpr int root = square_root(25);
    std::cout << "sqrt(25) = " << root << '\n';

    return 0;
}

5. 最佳实践建议

场景 推荐使用
需要兼容 C++17 及更早编译器 constexpr
想在编译期强制验证输入合法性 consteval
需要在运行时也能调用同一函数 constexpr
想在编译期捕获错误,防止错误实例化 consteval
想利用编译器的编译期优化但不必强制 constexpr

6. 小结

  • constexpr:灵活、兼容、可在编译期或运行时求值;适用于需要在多处使用的常量或函数。
  • consteval:严格、强制编译期求值;适用于必须在编译期验证或生成错误的情形。

在实际项目中,先考虑兼容性与使用场景,再决定使用哪种关键字。若你只需要在编译期计算常量,使用 constexpr 即可;若你想在编译期捕获错误或强制编译期求值,选择 consteval。这样既能充分利用编译期计算的优势,又能保持代码的可维护性与可读性。

发表评论