C++17 引入的 std::variant 和 std::visit 让我们可以在不使用传统虚函数机制的情况下实现类型安全的多态。本文将演示如何通过这两个工具来创建一种更轻量级且更易于维护的多态方案,并对比传统虚函数实现的优缺点。
1. 为什么不使用虚函数?
传统的多态通常通过基类指针和虚函数实现:
struct Shape { virtual void draw() = 0; };
struct Circle : Shape { void draw() override {...} };
struct Square : Shape { void draw() override {...} };
缺点:
- 多态开销:每个对象都要存储虚表指针(vptr),导致内存占用增加。
- 继承层次深度:大量基类层次会导致维护困难。
- 类型安全性不足:运行时才会发生类型错误。
2. std::variant 与 std::visit 的基本概念
std::variant<Ts...>是一个可以存放若干种类型中任意一种的类型安全容器。它类似于union,但提供了完整的类型安全和易用的 API。std::visit是一个多态访问器,接受一个std::variant和一个可调用对象(通常是 lambda 或std::function),根据当前持有的类型调用对应的处理逻辑。
3. 示例:绘制不同形状
#include <variant>
#include <iostream>
#include <string>
#include <vector>
struct Circle {
double radius;
void draw() const { std::cout << "Circle: r=" << radius << '\n'; }
};
struct Square {
double side;
void draw() const { std::cout << "Square: s=" << side << '\n'; }
};
struct Triangle {
double base, height;
void draw() const { std::cout << "Triangle: b=" << base << " h=" << height << '\n'; }
};
using Shape = std::variant<Circle, Square, Triangle>;
3.1 创建形状集合
std::vector <Shape> shapes = {
Circle{5.0},
Square{3.0},
Triangle{4.0, 6.0}
};
3.2 使用 std::visit 统一绘制
for (const auto& s : shapes) {
std::visit([](auto&& shape) {
shape.draw();
}, s);
}
输出:
Circle: r=5
Square: s=3
Triangle: b=4 h=6
4. 进一步扩展:实现多种行为
由于 std::visit 接受可调用对象,我们可以轻松添加多种行为,例如计算面积、序列化等:
auto area = [](auto&& shape) {
using T = std::decay_t<decltype(shape)>;
if constexpr (std::is_same_v<T, Circle>) {
return 3.14159 * shape.radius * shape.radius;
} else if constexpr (std::is_same_v<T, Square>) {
return shape.side * shape.side;
} else if constexpr (std::is_same_v<T, Triangle>) {
return 0.5 * shape.base * shape.height;
}
};
double total_area = 0;
for (const auto& s : shapes) {
total_area += std::visit(area, s);
}
std::cout << "Total area: " << total_area << '\n';
5. 优点总结
| 特点 | 传统虚函数 | std::variant + std::visit |
|---|---|---|
| 内存占用 | 每个对象存 vptr | 无额外 vptr |
| 编译时类型检查 | 仅在基类层面 | 完全类型安全 |
| 可扩展性 | 需修改基类 | 仅需添加新类型 |
| 运行时开销 | 虚函数表查表 | 直接调用 lambda |
| 继承层次 | 可能深 | 通过 std::variant 列表 |
6. 注意事项
std::variant需要在编译时知道所有可能的类型,不能像传统多态那样动态添加新子类。- 对于非常大或复杂的类型树,维护 variant 列表可能变得繁琐。
std::visit的可调用对象必须兼容所有类型,否则会编译错误,正好提升了类型安全。
7. 结语
std::variant 与 std::visit 为 C++17 及以后提供了一种更现代、更轻量级的多态实现方式。它们在不需要传统继承层次的场景中尤为适用,尤其是在数据驱动、配置驱动或插件化架构中。通过这种方式,我们可以减少运行时开销,提升代码的可读性与可维护性。祝你编码愉快!