在 C++17 之后,标准库提供了 std::variant 作为类型安全的多重容器,能够在运行时存储多种不同类型中的任意一种,并且保证类型检查。相比传统的指针多态,std::variant 既避免了运行时的类型转换错误,又不需要虚表开销。下面我们通过一个简单的例子,演示如何用 std::variant 实现一个“多态”集合,并使用 std::visit 访问不同类型的元素。
1. 基本思路
- 定义多态基类:如果你已有一个基类
Shape,可以让子类Circle、Rectangle等继承它。 - 使用 std::variant:将
std::variant<Circle, Rectangle, Triangle>定义为ShapeVariant,即它可以保存上述任意一种形状。 - 访问元素:使用
std::visit并提供一个可调用对象(lambda 或 struct)来处理每种具体类型。
2. 示例代码
#include <iostream>
#include <vector>
#include <variant>
#include <cmath>
// 基础形状接口
struct Shape {
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
// 圆
struct Circle : Shape {
double radius;
Circle(double r) : radius(r) {}
double area() const override { return M_PI * radius * radius; }
void draw() const override { std::cout << "Circle(radius=" << radius << ")\n"; }
};
// 矩形
struct Rectangle : Shape {
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
void draw() const override { std::cout << "Rectangle(width=" << width << ", height=" << height << ")\n"; }
};
// 三角形
struct Triangle : Shape {
double base, height;
Triangle(double b, double h) : base(b), height(h) {}
double area() const override { return 0.5 * base * height; }
void draw() const override { std::cout << "Triangle(base=" << base << ", height=" << height << ")\n"; }
};
// 通过 std::variant 存储多种形状
using ShapeVariant = std::variant<Circle, Rectangle, Triangle>;
int main() {
// 创建一个形状集合
std::vector <ShapeVariant> shapes;
shapes.emplace_back(Circle{5.0});
shapes.emplace_back(Rectangle{4.0, 6.0});
shapes.emplace_back(Triangle{3.0, 4.0});
double totalArea = 0.0;
// 访问每个元素
for (const auto& sv : shapes) {
// 使用 std::visit 访问当前类型
std::visit([&](auto&& shape) {
// 通过 static_cast 取得具体类型指针
// (若需要访问 Shape 接口,可改用 std::apply 结合基类指针)
std::cout << "Drawing: ";
shape.draw();
std::cout << "Area: " << shape.area() << "\n\n";
totalArea += shape.area();
}, sv);
}
std::cout << "Total area: " << totalArea << "\n";
return 0;
}
代码说明
- 定义三种形状:
Circle、Rectangle、Triangle均继承自纯虚基类Shape,实现area()与draw()。 ShapeVariant:使用std::variant定义可容纳任意三种形状的容器。std::visit:遍历shapes向量时,使用std::visit,提供一个 lambda。auto&& shape自动推断当前元素的实际类型,进而调用对应的成员函数。
3. 与传统多态的比较
| 特点 | 传统指针多态 | std::variant |
|---|---|---|
| 运行时开销 | 虚表查表,指针解引用 | 访问 variant 时可能有跳表,但无虚表 |
| 类型安全 | dynamic_cast 可能失败 |
编译时确定可存储类型,运行时不需要强制转换 |
| 可移植性 | 需要手动删除对象 | variant 内部直接存储对象,无需手动释放 |
| 可读性 | 需要基类、派生类 | variant 与 visit 代码更紧凑 |
4. 常见错误与调试
- 忘记给
variant指定所有可能类型:编译报错,需确保variant<...>中包含所有需要使用的类型。 - 在
std::visit的 lambda 中使用auto&&但未调用shape.area():编译错误,确保 lambda 内部使用正确类型的成员。 - 如果想在
std::visit外部访问同一对象:需要先 `std::holds_alternative (variant)` 判断后再 `std::get(variant)`。
5. 进阶:使用 std::visit 与 polymorphism
如果你仍想保留基类接口但用 variant 存储,你可以在 variant 的 lambda 中通过 std::apply 或者直接使用 static_cast<const Shape&>(shape) 访问基类接口。例如:
std::visit([&](auto&& shape) {
const Shape& base = static_cast<const Shape&>(shape);
base.draw(); // 调用虚函数
totalArea += base.area();
}, sv);
这样即使使用 variant,仍可通过基类接口统一调用。
6. 结语
std::variant 是 C++17 引入的强大工具,适合在不需要虚表开销、且类型集合已知且有限的场景下使用。通过与 std::visit 配合,可以轻松实现类型安全的“多态”行为。希望本文对你理解和使用 std::variant 有所帮助。