C++20 模板中 constexpr 与 consteval 的区别与应用

在 C++20 标准中,constexprconsteval 两个关键字都与常量表达式(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);
}

fibconsteval,保证编译期递归展开;如果用 constexprstatic_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 用于给全局/静态变量强制在编译期初始化,而不保证变量本身是常量。它经常与 constexprconsteval 结合使用:

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 的模板世界中玩得愉快!

发表评论