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 上下文中使用
new和delete - 但分配的内存必须在编译期求值结束前全部释放
- 不能有内存泄漏,不能有悬垂指针
我曾经在写一个编译期 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,你得用模板递归和变参模板才能做到。现在用虚函数和智能指针,代码直观多了。
四、知识体系图
下面这张图总结了这两个特性之间的关系和应用场景:
五、实际应用中的注意事项
这两个特性虽然强大,但也不是银弹。我总结了几条经验:
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 编译期算法选择 | 用 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 接口,让用户在编译期完成配置、校验、甚至代码生成。嗯,这确实是个值得期待的方向。