一、从虚表到静态多态:C++17/20 带来的新思路

说实话,我写 C++ 十几年了,早期做多态设计时,脑子里只有虚函数和继承。直到 C++17/20 出来,我才发现——原来多态可以不用虚表,甚至不用继承。

你想想看,传统的虚函数多态,说白了就是运行时通过 vptr 查虚表。这招虽然好用,但代价也不小:每个对象多一个指针,虚表查找有开销,而且类层次一旦复杂,维护起来真是头疼。

我在项目中遇到过几次这样的场景:一个接口类有十几个虚函数,子类只重写其中两三个,剩下的全是空实现。嗯,这种代码看着就难受。后来我改用 std::variant + 访问者模式,代码量直接砍半。

这一章,我们就来聊聊 C++17/20 给多态带来的新武器。它们不是要取代虚函数,而是给你更多选择。

核心观点: C++17/20 引入了“静态多态”的新范式——编译期就能确定调用哪个函数,不需要虚表,不需要运行时开销。

二、std::variant:类型安全的联合体

2.1 它解决了什么问题?

传统的 union 有个大坑:你得自己记住当前存的是哪个类型。我见过不少 bug 就是因为写入了 int 却读成了 float,编译器还不报错。

std::variant 就是类型安全的 union。它知道当前存的是什么类型,访问错了会抛异常。

#include <variant>
#include <iostream>

std::variant<int, double, std::string> v;

v = 42;                    // 存 int
std::cout << std::get<int>(v);  // 正确

v = 3.14;                  // 改成 double
// std::get<int>(v);       // 运行时抛 std::bad_variant_access

// 安全访问方式
if (auto* p = std::get_if<double>(&v)) {
    std::cout << "double: " << *p;
}
我的习惯: 能用 std::get_if 就别用 std::get。前者返回指针,失败返回 nullptr,比抛异常优雅得多。

2.2 访问者模式:让 variant 动起来

光能存不同类型还不够,我们还得对它们做操作。这就是 std::visit 登场的时候了。

struct Visitor {
    void operator()(int i) const {
        std::cout << "int: " << i << "\n";
    }
    void operator()(double d) const {
        std::cout << "double: " << d << "\n";
    }
    void operator()(const std::string& s) const {
        std::cout << "string: " << s << "\n";
    }
};

std::variant<int, double, std::string> v = "hello";
std::visit(Visitor{}, v);  // 输出: string: hello

这其实就是访问者模式在 C++ 里的现代实现。你想想看,传统做法得写一个基类、几个子类、再加一个 accept 函数。现在一个 variant + 一个 visitor 就搞定了。

我曾经踩过的坑: 访问者里的每个 operator() 必须覆盖 variant 的所有类型,少一个编译不过。C++20 可以用泛型 lambda 偷懒:auto visitor = [](auto&& arg) { /* 通用处理 */ };

三、std::any:真正的“任意类型”容器

std::any 比 variant 更灵活——它可以存任何类型,不限于你预先列出的那些。代价是运行时开销更大。

#include <any>

std::any a = 42;
a = std::string("hello");
a = 3.14;

// 取出时必须知道原类型
try {
    auto val = std::any_cast<double>(a);
    std::cout << val;
} catch (const std::bad_any_cast& e) {
    std::cerr << "类型不匹配";
}

// 更安全的做法
if (auto* p = std::any_cast<double>(&a)) {
    std::cout << *p;
}

我个人觉得 std::any 适合做“类型擦除”的场景。比如插件系统里,你不想让主程序知道插件的具体类型,但又想存下来。这时候 any 就比 void* 安全得多。

性能提醒: std::any 内部会做动态内存分配(对小对象可能有 SSO 优化)。如果你知道所有可能的类型,用 std::variant 更高效。

四、std::function:函数的多态容器

std::function 其实也是一种多态——它用类型擦除技术,让你可以存任何可调用对象,只要签名匹配。

#include <functional>

std::function<int(int, int)> func;

func = [](int a, int b) { return a + b; };  // lambda
func = std::multiplies<int>{};               // 函数对象
func = [](int a, int b) { return a * a + b; }; // 另一个 lambda

// 调用时完全不知道内部是什么
int result = func(3, 4);

我在项目中常用 std::function 做回调注册。比如一个事件系统,不同模块注册不同的处理函数,统一用 function 存起来。调用方不需要关心具体是 lambda、函数指针还是成员函数。

避坑指南: std::function 有轻微开销(虚函数调用 + 可能的堆分配)。如果性能敏感,考虑用模板或 auto 参数。另外,捕获了 shared_ptr 的 lambda 放进 function 里容易造成循环引用,记得用 weak_ptr。

五、Concepts:编译期多态的“契约”

C++20 的 Concepts 是我最期待的特性之一。它让模板的错误信息从“天书”变成了人话。

传统模板多态的问题是:你传错了类型,编译器给你报几十行错误,全是模板实例化内部的细节。有了 Concepts,你可以明确说“这个类型必须支持这些操作”。

#include <concepts>

// 定义一个 concept:可比较大小
template <typename T>
concept Comparable = requires(T a, T b) {
    { a < b } -> std::convertible_to<bool>;
    { a == b } -> std::convertible_to<bool>;
};

// 使用 concept 约束模板
template <Comparable T>
T max(T a, T b) {
    return (a < b) ? b : a;
}

// 错误示例:传一个不支持 < 的类型
// struct Point { int x, y; };
// max(Point{1,2}, Point{3,4}); // 编译错误,提示清晰

你想想看,这其实就是一种“编译期多态”——在编译阶段就检查类型是否满足接口要求。和虚函数多态不同,它没有运行时开销,而且约束更精确。

我的经验: 写模板库时,给每个公共模板加上 concept 约束。一开始可能觉得麻烦,但维护半年后你会发现——编译错误信息从“天书”变成了“这里写错了”,团队效率提升明显。

六、知识体系总览

下面这张图总结了 C++17/20 中多态相关的新特性,以及它们各自的应用场景。

C++17/20 多态新特性全景图 多态新范式 std::variant std::any std::function Concepts 类型安全 union + 访问者模式 编译期确定类型 任意类型容器 运行时类型擦除 灵活但开销大 可调用对象容器 类型擦除 + 多态 回调/事件系统 编译期接口约束 模板多态契约 零运行时开销 与传统虚函数多态的对比 虚函数:运行时多态 有 vptr 开销 新特性:编译期/静态多态 零或更低运行时开销 适用场景互补 可混合使用

七、总结与选择建议

说了这么多,到底什么时候用哪个?我根据自己的经验给个参考:

场景 推荐方案 理由
类型集合固定(如 AST 节点) std::variant + std::visit 编译期确定,无堆分配,性能好
类型完全未知(如插件系统) std::any 灵活性最高,但注意性能
回调/事件/策略模式 std::function 类型擦除,接口统一
模板接口约束 Concepts 编译期检查,零开销
需要运行时多态(如 GUI 框架) 传统虚函数 虚函数仍然是正确选择
我的建议: 别急着用新特性替代所有虚函数。先评估场景——如果类型集合固定且不大,variant 是首选;如果需要运行时扩展,虚函数依然是最稳的方案。C++17/20 给了你更多工具,但选对工具才是关键。

嗯,这一章就到这里。记住一点:多态的本质是“同一接口,不同实现”。虚函数是一种实现方式,variant + visit 是另一种,Concepts 又是一种。理解它们的适用场景,比死记语法更重要。


公众号:蓝海资料掘金营,微信deep3321