在 C++17 之前,想要在同一个容器中存放不同类型的数据,往往需要使用 void*、boost::variant 或手写类型擦除(type erasure)等手段。随着 C++17 引入 std::variant,我们可以在编译期就保证类型安全,避免了运行时的错误。下面,我们通过一个简单的例子来演示如何利用 std::variant 实现多态的功能,并在此基础上讨论它的优势与局限。
1. 背景:多态的传统实现
传统的多态实现主要有两类:
-
继承+虚函数
struct Shape { virtual void draw() const = 0; }; struct Circle : Shape { void draw() const override { /* ... */ } }; struct Square : Shape { void draw() const override { /* ... */ } }; -
类型擦除(type erasure)
例如std::any或自定义的包装类,用来存储任意类型,但需要手动检查类型。
虽然这两种方式都能实现多态,但前者需要继承体系,后者缺乏编译期类型检查,容易出现类型不匹配错误。
2. std::variant 的核心概念
std::variant<T1, T2, ..., TN> 是一种可变类型,内部保持一个 union 并记录当前值的类型。它提供:
- 编译期类型安全:编译器会检查访问的类型是否在列表中。
- 访问方式:`std::get (var)` 或 `std::get_if(&var)`。
- 访问器:
std::visit通过访客(visitor)模式统一访问。
3. 代码示例:图形绘制
我们用 std::variant 来替代传统的继承方式,存放不同图形对象,并统一调用 draw()。
#include <iostream>
#include <variant>
#include <vector>
#include <string>
// ① 定义图形类型
struct Circle { double radius; };
struct Square { double side; };
struct Triangle { double base, height; };
// ② 让每个图形都实现一个 draw() 函数
namespace ShapeOps {
void draw(const Circle& c) {
std::cout << "Circle: radius = " << c.radius << '\n';
}
void draw(const Square& s) {
std::cout << "Square: side = " << s.side << '\n';
}
void draw(const Triangle& t) {
std::cout << "Triangle: base = " << t.base << ", height = " << t.height << '\n';
}
}
// ③ 用 std::variant 包装所有图形
using ShapeVariant = std::variant<Circle, Square, Triangle>;
// ④ 访问器(Visitor)
struct DrawVisitor {
void operator()(const Circle& c) const { ShapeOps::draw(c); }
void operator()(const Square& s) const { ShapeOps::draw(s); }
void operator()(const Triangle& t) const { ShapeOps::draw(t); }
};
int main() {
std::vector <ShapeVariant> shapes;
shapes.emplace_back(Circle{5.0});
shapes.emplace_back(Square{3.0});
shapes.emplace_back(Triangle{4.0, 2.5});
for (const auto& shape : shapes) {
std::visit(DrawVisitor{}, shape);
}
return 0;
}
运行结果
Circle: radius = 5
Square: side = 3
Triangle: base = 4, height = 2.5
4. 与继承+虚函数的对比
| 方面 | 继承+虚函数 | std::variant |
|---|---|---|
| 内存布局 | 对象表指针(vptr) | union + index |
| 类型安全 | 运行时检查 | 编译时检查 |
| 多态性能 | 虚函数调用(间接) | 访客访问(模板展开) |
| 可扩展性 | 需要修改基类 | 只需添加新类型 |
| 代码可读性 | 继承层次清晰 | 需要访客模式 |
- 优势:
std::variant在编译期决定类型,避免了虚函数的间接调用;当图形种类固定且数量有限时,访客模式比继承更直观。 - 局限:若需要在运行时动态新增图形类型,
variant就不适用;且variant的类型列表必须在编译期确定。
5. 进一步思考:如何在更大规模系统中使用
-
组合模式
`。
对于需要组合多种图形的场景,可将variant嵌套或使用 `std::vector -
访问器自动生成
通过宏或模板生成DrawVisitor,减少手动编写多种operator()。 -
与
std::any、std::function混合
若某些图形需要额外的行为(例如回调),可以在variant内部再包装std::function。
6. 小结
std::variant 为 C++ 提供了一种类型安全、编译期可知的多态机制,适用于类型集合已知且固定的场景。通过 std::visit 和访客模式,代码既保持了多态的灵活性,又避免了传统虚函数调用带来的间接开销和运行时错误。掌握这一特性,能够在不牺牲类型安全的前提下,写出更高效、更易维护的 C++ 代码。