在 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;
}
关键点解析
-
定义结构体
每种形状都有自己的属性与成员函数,保持了单一职责。成员函数area()与perimeter()的返回类型统一为double,方便后续统一处理。 -
使用
std::variant
using Shape = std::variant<Circle, Rectangle, Triangle>;
通过std::variant可以在运行时安全地存储三种形状中的任意一种,且在编译阶段已知可能的类型。 -
访问变体
std::visit是访问std::variant的推荐方式。它接受一个可调用对象(lambda),参数是变体当前持有的值。由于使用了通用引用auto&& arg,可以在访问时保留值的完整性。 -
类型识别
typeid(T).name()仅作演示用途,在生产环境可以结合std::type_info或自定义名称映射来获取更友好的字符串。 -
性能与安全
std::variant不需要虚表,内存占用更小,访问更快。- 编译器会检查
std::visit中传入的可调用对象是否覆盖了所有变体类型,避免遗漏。 - 变体可以与
std::optional、std::any等一起使用,进一步构建灵活而安全的类型系统。
与传统多态比较
| 特性 | 虚表多态 | std::variant |
|---|---|---|
| 运行时开销 | 虚表查找 | 直接索引(常量时间) |
| 编译时类型检查 | 需要 RTTI 或 dynamic_cast |
完全在编译期完成 |
| 对继承层级限制 | 可以是任意继承结构 | 必须是非继承关系,结构体/类互不关联 |
| 可读性 | 通过基类方法 | 通过 std::visit 统一处理 |
| 可组合性 | 需要基类层次 | 通过 std::variant 组合多种类型 |
在需要多种“同类”对象、且不想牺牲性能和类型安全时,std::variant 是一种非常优雅的解决方案。只要保证每个类型实现相同的接口(如 area()、perimeter()),后续的统一操作都变得异常简洁。