如何使用 std::variant 实现类型安全的多态?

在 C++17 之后,标准库提供了 std::variant 作为类型安全的多重容器,能够在运行时存储多种不同类型中的任意一种,并且保证类型检查。相比传统的指针多态,std::variant 既避免了运行时的类型转换错误,又不需要虚表开销。下面我们通过一个简单的例子,演示如何用 std::variant 实现一个“多态”集合,并使用 std::visit 访问不同类型的元素。

1. 基本思路

  • 定义多态基类:如果你已有一个基类 Shape,可以让子类 CircleRectangle 等继承它。
  • 使用 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;
}

代码说明

  1. 定义三种形状CircleRectangleTriangle 均继承自纯虚基类 Shape,实现 area()draw()
  2. ShapeVariant:使用 std::variant 定义可容纳任意三种形状的容器。
  3. std::visit:遍历 shapes 向量时,使用 std::visit,提供一个 lambda。auto&& shape 自动推断当前元素的实际类型,进而调用对应的成员函数。

3. 与传统多态的比较

特点 传统指针多态 std::variant
运行时开销 虚表查表,指针解引用 访问 variant 时可能有跳表,但无虚表
类型安全 dynamic_cast 可能失败 编译时确定可存储类型,运行时不需要强制转换
可移植性 需要手动删除对象 variant 内部直接存储对象,无需手动释放
可读性 需要基类、派生类 variantvisit 代码更紧凑

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 有所帮助。

发表评论