C++ 中的 constexpr 与编译期计算:从 C++11 到 C++23 的演进

在 C++11 引入 constexpr 的时候,它的功能仅仅是让函数在编译期可求值,且只能包含单一 return 语句、没有循环或递归等复杂控制流。随后 C++14 扩大了其能力,允许在 constexpr 函数中使用变量声明、循环和递归,甚至支持异常处理。C++17 进一步改进,允许 constexpr 变量在声明时进行初始化,同时支持非静态数据成员的 constexpr 初始化。到 C++20,constexpr 彻底变成了“常量表达式的通用工具”,任何在编译期能得到值的表达式都可以用 constexpr 修饰,甚至可以在类模板内部使用 constexpr 产生编译期生成的成员。C++23 则将 constexpr 函数的体裁进一步宽松到几乎任何合法的函数体,并允许在 constexpr 中使用动态异常处理、虚函数调用等,极大提升了其表达力。

以下代码展示了从 C++11 到 C++23 对 constexpr 的演进,并阐述了它在实际项目中的应用场景。

#include <iostream>
#include <array>
#include <cmath>
#include <concepts>
#include <utility>

// C++11 constexpr:单一 return 语句
constexpr int square_old(int x) {
    return x * x;
}

// C++14 constexpr:循环、递归
constexpr int factorial(int n) {
    return n <= 1 ? 1 : (n * factorial(n - 1));
}

// C++17 constexpr:在类内部使用
struct Matrix {
    constexpr Matrix(int rows, int cols) : rows(rows), cols(cols), data(rows * cols) {}
    constexpr double& operator()(int r, int c) { return data[r * cols + c]; }
    int rows, cols;
    std::vector <double> data;
};

// C++20 constexpr:constexpr 变量、泛型
template <typename T>
constexpr T max_cpp20(const T& a, const T& b) {
    return a > b ? a : b;
}

// C++23 constexpr:支持异常、虚函数
class Base {
public:
    virtual constexpr double get_value() const = 0;
};

class Derived : public Base {
public:
    constexpr double get_value() const override { return 42.0; }
};

int main() {
    constexpr int sq = square_old(5);            // 编译期求值
    constexpr int fact = factorial(5);           // 递归编译期计算
    constexpr Matrix m(2, 3);                    // 结构体 constexpr 构造
    m(0, 1) = 3.14;                               // 运行期修改
    constexpr double val = max_cpp20(3.14, 2.71); // 模板 constexpr
    constexpr Derived d;
    constexpr double derived_val = d.get_value(); // 虚函数 constexpr

    std::cout << "square: " << sq << "\n";
    std::cout << "factorial: " << fact << "\n";
    std::cout << "matrix[0][1]: " << m(0, 1) << "\n";
    std::cout << "max: " << val << "\n";
    std::cout << "derived value: " << derived_val << "\n";
}

主要收益

  1. 编译期安全:constexpr 让常量表达式在编译期求值,避免了运行时错误。例如,std::array 的大小可以直接由 constexpr 计算得到,从而在编译阶段检查边界错误。
  2. 性能提升:对经常使用的计算(如多项式、斐波那契数列、数学常数等)使用 constexpr 可以消除运行时计算。
  3. 模板元编程的简化:传统的元编程往往依赖于 std::integral_constantstd::enable_if 等技术,constexpr 的普及让函数式编程成为可能,代码更加直观。
  4. 更好的错误诊断:如果 constexpr 计算失败,编译器会报错,帮助开发者在编译期捕捉错误,而不是等到运行时。

注意事项

  • 过度使用 constexpr 可能导致编译时间显著增加,尤其是在包含大量递归或循环的 constexpr 函数时。
  • constexpr 变量必须在声明时完成初始化,且初始化值必须是常量表达式。
  • 虽然 C++23 允许在 constexpr 中使用虚函数,但该调用仍会在编译期被解析为静态多态,运行时并不会产生虚调用开销。

通过合理规划 constexpr 的使用,能够让 C++ 代码更安全、更高效、更易维护。

发表评论