constexpr 与 consteval:C++20 何时选择每一种

在 C++20 中,constexprconsteval 都允许在编译时进行计算,但它们的语义和使用场景有显著差异。下面我们从概念、限制、典型用例以及性能影响四个角度来剖析这两者,帮助你在编写高效、可读、可靠的 C++ 代码时做出正确选择。

1. 基本定义

关键词 允许的上下文 计算时机 运行时可用性
constexpr 变量、函数、构造函数、类成员、模板非类型参数等 既可在编译时也可在运行时 运行时可见
consteval 函数(不包括构造函数) 必须在编译时 仅编译时可见
  • constexpr 语义:若在编译时可以完成计算,则在编译时计算;否则在运行时计算。编译器会根据调用位置决定是否使用编译时执行。
  • consteval 语义:调用点必须满足“必须在编译时求值”。若编译器无法在编译时计算,程序即编译失败。

2. 语法与约束

2.1 constexpr

constexpr int square(int x) {
    return x * x;
}
  • 允许递归、循环(C++20 引入 constexpr 循环),但必须满足所有语句在编译时可求值。
  • 对象可声明为 constexpr,但需要满足构造函数也为 constexpr
  • 可以用作非类型模板参数。

2.2 consteval

consteval int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
  • 只能是函数(不能是变量、构造函数或类成员)。C++20 之前没有此限制,但标准已将其明确为仅函数。
  • 所有参数都必须在编译时可用(不允许传递运行时变量)。
  • 不允许递归调用返回值未在编译时完成时导致无限递归。

3. 典型用例

3.1 何时使用 constexpr

场景 说明
可变计算 需要在运行时或编译时都可能出现的数值,例如从配置文件读取并且在编译时有默认值。
模板非类型参数 std::array<int, N>,N 必须是 constexpr 表达式。
需要兼容旧编译器 constexpr 在 C++11 就已存在,consteval 仅在 C++20。
允许运行时使用 例如 constexpr std::string_view 在编译时生成常量,但在运行时也可使用。

3.2 何时使用 consteval

场景 说明
强制编译时求值 防止误用导致运行时错误,例如 constexpr 的函数在运行时可能被调用,使用 consteval 可以立即报错。
编译时验证 用于元编程,确保输入合法,例如编译时验证模板参数是否合法。
性能敏感 对于极其小而频繁调用的函数,使用 consteval 可避免潜在的运行时开销,尽管编译器通常会内联 constexpr
静态编译期检查 用于实现像 static_assert 那样的检查,但可以返回值并被直接使用。

4. 性能与可维护性对比

  • 性能:大多数现代编译器会在可能的情况下将 constexpr 函数内联并在编译时求值。区别主要在于是否强制编译时计算。若编译器无法在编译时求值,constexpr 可能导致运行时计算,consteval 则会导致编译错误。使用 consteval 时要确保所有调用点都满足编译时可求值,否则将导致编译失败。
  • 可维护性constexpr 语义更宽松,代码更易被重用。consteval 提供了更强的安全保障,适合库设计者想要确保某些接口只能在编译期使用的场景。

5. 实际案例

5.1 用 consteval 做数组大小检查

consteval std::size_t array_size(std::size_t n) {
    if (n == 0) throw "Size must be > 0";
    return n;
}

template<std::size_t N>
struct FixedBuffer {
    std::array<char, array_size(N)> buf;
};

任何 N==0 的实例都会在编译时报错。

5.2 用 constexpr 计算数学常量

constexpr double pi() {
    return 3.14159265358979323846;
}

double circumference(double radius) {
    return 2 * pi() * radius;   // 运行时可用
}

此函数在运行时可以接受任意 radius,但 pi() 本身在编译时已求值。

6. 小结

  • constexpr:适用于可在编译时也可在运行时求值的函数或变量,兼容性好,使用范围广。
  • consteval:强制编译时求值,适用于需要在编译期验证或避免运行时开销的场景。
  • 选择时请根据函数的调用位置、可用性和安全性需求做决定。

通过正确使用 constexprconsteval,可以在 C++20 及以后版本中编写更安全、更高效且易维护的代码。

发表评论