如何在 C++ 中使用 std::variant 实现类型安全的多态

在 C++17 之后,标准库提供了 std::variant,它是一个类型安全的联合体(类似于 Rust 的 enum),可以存储多种可能类型中的任意一种,并且在运行时能够安全地检查并访问当前存储的类型。相比传统的基类指针多态实现,std::variant 更加轻量,避免了虚表开销,并且在编译时就能验证类型的合法性。

下面通过一个完整示例,演示如何利用 std::variant 构建一个简单的“形状”系统:圆形、矩形和三角形,分别计算面积和周长,并通过统一的 Shape 变量访问。

#include <iostream>
#include <variant>
#include <cmath>
#include <string>
#include <vector>

// ---------- 定义形状结构 ----------
struct Circle {
    double radius;
    double area()   const { return M_PI * radius * radius; }
    double perimeter() const { return 2 * M_PI * radius; }
};

struct Rectangle {
    double width, height;
    double area()   const { return width * height; }
    double perimeter() const { return 2 * (width + height); }
};

struct Triangle {
    double a, b, c;                     // 三边长度
    double area()   const {              // 海伦公式
        double s = (a + b + c) / 2.0;
        return std::sqrt(s * (s - a) * (s - b) * (s - c));
    }
    double perimeter() const { return a + b + c; }
};

// ---------- Shape 变体 ----------
using Shape = std::variant<Circle, Rectangle, Triangle>;

// ---------- 访问器 ----------
template <typename T>
std::string type_name(const T&) { return typeid(T).name(); }

// ---------- 计算面积与周长 ----------
double total_area(const std::vector <Shape>& shapes) {
    double sum = 0.0;
    for (const auto& s : shapes) {
        std::visit([&sum](auto&& arg){ sum += arg.area(); }, s);
    }
    return sum;
}

double total_perimeter(const std::vector <Shape>& shapes) {
    double sum = 0.0;
    for (const auto& s : shapes) {
        std::visit([&sum](auto&& arg){ sum += arg.perimeter(); }, s);
    }
    return sum;
}

// ---------- 主函数 ----------
int main() {
    std::vector <Shape> shapes = {
        Circle{5.0},
        Rectangle{4.0, 6.0},
        Triangle{3.0, 4.0, 5.0}
    };

    std::cout << "总面积: " << total_area(shapes) << '\n';
    std::cout << "总周长: " << total_perimeter(shapes) << '\n';

    // 输出每个形状的类型
    for (const auto& s : shapes) {
        std::visit([](auto&& arg){
            std::cout << "当前形状: " << type_name(arg) << '\n';
        }, s);
    }

    return 0;
}

关键点解析

  1. 定义结构体
    每种形状都有自己的属性与成员函数,保持了单一职责。成员函数 area()perimeter() 的返回类型统一为 double,方便后续统一处理。

  2. 使用 std::variant
    using Shape = std::variant<Circle, Rectangle, Triangle>;
    通过 std::variant 可以在运行时安全地存储三种形状中的任意一种,且在编译阶段已知可能的类型。

  3. 访问变体
    std::visit 是访问 std::variant 的推荐方式。它接受一个可调用对象(lambda),参数是变体当前持有的值。由于使用了通用引用 auto&& arg,可以在访问时保留值的完整性。

  4. 类型识别
    typeid(T).name() 仅作演示用途,在生产环境可以结合 std::type_info 或自定义名称映射来获取更友好的字符串。

  5. 性能与安全

    • std::variant 不需要虚表,内存占用更小,访问更快。
    • 编译器会检查 std::visit 中传入的可调用对象是否覆盖了所有变体类型,避免遗漏。
    • 变体可以与 std::optionalstd::any 等一起使用,进一步构建灵活而安全的类型系统。

与传统多态比较

特性 虚表多态 std::variant
运行时开销 虚表查找 直接索引(常量时间)
编译时类型检查 需要 RTTI 或 dynamic_cast 完全在编译期完成
对继承层级限制 可以是任意继承结构 必须是非继承关系,结构体/类互不关联
可读性 通过基类方法 通过 std::visit 统一处理
可组合性 需要基类层次 通过 std::variant 组合多种类型

在需要多种“同类”对象、且不想牺牲性能和类型安全时,std::variant 是一种非常优雅的解决方案。只要保证每个类型实现相同的接口(如 area()perimeter()),后续的统一操作都变得异常简洁。

发表评论