37、C++20 核心特性:constexpr虚拟函数与constexpr动态分配,编译期能力大爆发

说实话,C++20 的这两个特性,让我在写代码时有种「豁然开朗」的感觉。

以前我们总说「编译期计算」是模板元编程的专利,写起来像在写外星语。但现在不一样了。constexpr 虚拟函数和 constexpr 动态分配,直接把编译期能力拉到了一个新高度。我个人觉得,这是 C++20 里最被低估的两个特性。

一、constexpr 虚拟函数:编译期的多态

先聊聊虚拟函数。你想想看,virtual 关键字一直意味着「运行时多态」。但 C++20 打破了这堵墙——现在你可以在 constexpr 上下文中调用虚函数了。

核心规则:只要对象的动态类型在编译期已知,虚函数调用就可以在 constexpr 求值中完成。

我在项目中遇到过这样一个场景:我们需要在编译期根据配置选择不同的算法实现。以前只能用 if constexpr 或者模板特化,代码写得很啰嗦。现在好了,直接定义一个抽象基类,让派生类实现 constexpr 虚函数,然后在编译期多态调用。

struct Shape {
    constexpr virtual double area() const = 0;
    constexpr virtual ~Shape() = default;
};

struct Circle : Shape {
    double radius;
    constexpr Circle(double r) : radius(r) {}
    constexpr double area() const override {
        return 3.1415926535 * radius * radius;
    }
};

struct Square : Shape {
    double side;
    constexpr Square(double s) : side(s) {}
    constexpr double area() const override {
        return side * side;
    }
};

constexpr double computeArea(const Shape& s) {
    return s.area();  // 编译期多态调用
}

int main() {
    constexpr Circle c(5.0);
    constexpr Square sq(4.0);
    constexpr double area1 = computeArea(c);  // 编译期计算
    constexpr double area2 = computeArea(sq);
    static_assert(area1 > 78.5 && area1 < 78.6);
    static_assert(area2 == 16.0);
}

嗯,这里要注意:虚函数必须是 constexpr 的,而且对象的生命周期必须在编译期可追踪。说白了,你不能在运行时 new 一个对象然后指望在 constexpr 里调用它的虚函数——那不行。

我的建议:如果你需要在编译期做策略模式或者状态模式,constexpr 虚函数是比模板更优雅的选择。代码可读性会好很多。

二、constexpr 动态分配:new 和 delete 也能在编译期用了

这个特性更炸裂。以前 constexpr 函数里不能动态分配内存,现在可以了——但有限制。

核心规则其实很简单:

  • 你可以在 constexpr 上下文中使用 newdelete
  • 但分配的内存必须在编译期求值结束前全部释放
  • 不能有内存泄漏,不能有悬垂指针

我曾经在写一个编译期 JSON 解析器时,被这个特性救了一命。以前只能用递归模板和类型萃取来解析,代码量巨大。现在可以直接在 constexpr 函数里用 vector 或者 string 了。

#include <memory>

constexpr int createAndUse() {
    auto ptr = std::make_unique<int>(42);
    int val = *ptr;
    // ptr 在函数结束前自动释放
    return val;
}

constexpr int result = createAndUse();
static_assert(result == 42);

你看,unique_ptr 也能在 constexpr 里用了。但要注意,不是所有标准库容器都支持。std::vector 在 C++20 里还不是完全 constexpr 的——这个坑我踩过。

避坑指南:我曾经在 constexpr 函数里用 std::vector,结果编译器报错。后来查了标准才知道,std::vector 的 constexpr 支持是 C++23 才完善的。C++20 里只有 std::unique_ptr 和 std::shared_ptr 的部分操作是 constexpr 的。

三、两者结合:编译期能力大爆发

当 constexpr 虚函数遇上 constexpr 动态分配,会发生什么?

你可以构建一个编译期的多态对象树,然后在编译期遍历它、计算它、甚至序列化它。这在以前想都不敢想。

struct Node {
    constexpr virtual int evaluate() const = 0;
    constexpr virtual ~Node() = default;
};

struct Literal : Node {
    int value;
    constexpr Literal(int v) : value(v) {}
    constexpr int evaluate() const override { return value; }
};

struct Add : Node {
    std::unique_ptr<Node> left, right;
    constexpr Add(std::unique_ptr<Node> l, std::unique_ptr<Node> r)
        : left(std::move(l)), right(std::move(r)) {}
    constexpr int evaluate() const override {
        return left->evaluate() + right->evaluate();
    }
};

constexpr int buildAndEval() {
    auto expr = Add(
        std::make_unique<Literal>(3),
        std::make_unique<Literal>(4)
    );
    return expr.evaluate();
}

constexpr int result = buildAndEval();
static_assert(result == 7);

这个例子展示了编译期的表达式树求值。放在 C++17,你得用模板递归和变参模板才能做到。现在用虚函数和智能指针,代码直观多了。

四、知识体系图

下面这张图总结了这两个特性之间的关系和应用场景:

C++20 编译期能力大爆发 constexpr 虚拟函数 编译期多态调用 constexpr 动态分配 编译期 new/delete 能力:编译期对象构造、多态调用、内存管理 不再需要模板元编程的「黑魔法」 编译期表达式求值 编译期策略模式 编译期序列化/解析 效果:更少的运行时开销,更安全的编译期代码

五、实际应用中的注意事项

这两个特性虽然强大,但也不是银弹。我总结了几条经验:

场景 推荐做法 不推荐做法
编译期算法选择 用 constexpr 虚函数 + 继承 用 if constexpr + 模板特化(可读性差)
编译期容器操作 用 std::array + constexpr 函数 用 std::vector(C++20 不支持)
编译期对象生命周期 用 unique_ptr 自动管理 手动 new/delete(容易漏释放)
调试编译期代码 用 static_assert 逐步验证 直接写复杂逻辑(出错了难排查)

一个小技巧:我习惯在 constexpr 函数里加一些 static_assert 来验证中间结果。比如计算到一半时,assert 一下某个值是不是预期的。这样能快速定位问题,不用等整个编译期求值完成才发现错误。

六、总结

constexpr 虚拟函数和 constexpr 动态分配,让 C++20 的编译期编程从「模板元编程的黑暗时代」走进了「现代 C++ 的光明大道」。你不再需要写那些晦涩的模板递归和类型萃取,直接用普通的虚函数和智能指针就能在编译期完成复杂的计算。

我个人觉得,这两个特性会改变我们写库的方式。以后会有越来越多的库提供 constexpr 接口,让用户在编译期完成配置、校验、甚至代码生成。嗯,这确实是个值得期待的方向。


专注资料整理