在 C++17 之后,标准库新增了 std::variant,它提供了一种在编译期确定值类型、运行时可以存储多种类型且保持类型安全的容器。与传统的 std::any 或基于继承实现的多态对象相比,std::variant 更加轻量、可预测,且在访问时不需要 RTTI。下面我们通过一个完整示例,演示如何使用 std::variant 来构建一个类型安全的多态容器,并说明其常见使用场景、性能考虑以及最佳实践。
1. 基本概念
-
定义
std::variant<Ts...> v;variant只能存放Ts...中列出的类型之一,且在任何时刻仅能保持一种类型。 -
访问
- `std::get (v)` 直接获取指定类型(如果当前类型不匹配,则抛出 `std::bad_variant_access`)。
- `std::get_if (&v)` 返回指针,如果类型匹配则返回指针,否则返回 `nullptr`。
std::visit通过访客模式统一处理所有可能类型。
-
特点
- 类型安全:编译时保证只能存入允许的类型。
- 无 RTTI:访问类型不需要运行时类型信息。
- 值语义:复制和移动行为符合普通对象。
2. 示例:实现一个“形状”容器
假设我们需要一个容器来存储多种几何形状(圆、矩形、三角形),并对每种形状执行统一的操作(面积计算、绘制等)。使用继承实现多态会产生虚函数表、动态分配等开销;使用 std::variant 可以保持值语义且无 RTTI。
#include <iostream>
#include <variant>
#include <vector>
#include <cmath>
// ① 定义几何形状
struct Circle {
double radius;
double area() const { return M_PI * radius * radius; }
};
struct Rectangle {
double width, height;
double area() const { return 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));
}
};
// ② 声明 variant 类型
using Shape = std::variant<Circle, Rectangle, Triangle>;
// ③ 统一访问方式(使用访客)
struct AreaVisitor {
double operator()(const Circle& c) const { return c.area(); }
double operator()(const Rectangle& r) const { return r.area(); }
double operator()(const Triangle& t) const { return t.area(); }
};
int main() {
// ④ 创建容器并填充
std::vector <Shape> shapes;
shapes.emplace_back(Circle{5.0});
shapes.emplace_back(Rectangle{3.0, 4.0});
shapes.emplace_back(Triangle{3.0, 4.0, 5.0});
// ⑤ 计算面积
for (const auto& shape : shapes) {
double a = std::visit(AreaVisitor{}, shape);
std::cout << "Area: " << a << std::endl;
}
}
输出
Area: 78.5398
Area: 12
Area: 6
3. 进阶使用
3.1 std::variant 与 std::monostate
std::monostate 是一个空类型,常用来表示“空值”。将它加入 variant 的类型列表,可以让容器具有可空特性。
using OptionalShape = std::variant<std::monostate, Circle, Rectangle, Triangle>;
3.2 std::visit 的多参数
std::visit 可以接受多个 variant,并将它们对应类型传递给访客。
std::visit([](auto&& s1, auto&& s2){
// 这里 s1, s2 分别是对应 variant 中的实际类型
}, shape1, shape2);
3.3 自定义访客(模板)
为了避免为每个类型写单独的重载,可以利用模板自动匹配:
template<class Visitor>
auto auto_visit(const Visitor& v, const Shape& s) {
return std::visit(v, s);
}
4. 性能与对比
| 对比点 | std::variant |
传统继承 + 虚函数 | std::any |
|---|---|---|---|
| 内存占用 | 仅占最大类型大小 + 对齐 | 需要 vptr + 动态分配 | 需要对象头 + 可能的复制 |
| 访问开销 | 常量时间 | 虚函数表查找 | 动态类型判断 |
| 类型安全 | 编译期检查 | 运行时 RTTI | 运行时检查 |
| 可变性 | 值语义 | 指针/引用 | 值语义 |
在绝大多数需要“和而不同”对象的场景中,std::variant 都能提供更好的性能和可维护性。
5. 设计建议
- 保持类型列表简短
variant内部会为每种类型维护栈帧开销,列表过长会影响编译速度和内存对齐。 - 避免过度嵌套
过深的variant嵌套会导致代码复杂且不易维护。 - 使用
std::in_place_type_t或std::in_place_index_t明确构造Shape s{ std::in_place_type <Circle>, 2.0 }; - 结合
std::visit与结构化绑定std::visit([](auto&& shape){ if constexpr (std::is_same_v<decltype(shape), Circle>) { // 处理圆 } else if constexpr (std::is_same_v<decltype(shape), Rectangle>) { // 处理矩形 } }, s);
6. 常见问题
| 问题 | 解决方案 |
|---|---|
怎样在 std::variant 内存储自定义类型的指针? |
直接存储指针类型即可,但要注意所有指针的生命周期。 |
std::variant 能否存储 std::function? |
可以,但 std::function 可能会产生复制/移动开销。 |
如何处理 variant 中的异常抛出? |
std::visit 本身会转发异常,使用 try-catch 处理即可。 |
7. 结语
std::variant 作为 C++17 标准库的一员,为程序员提供了一个强大、轻量且类型安全的多态容器。它在实现值语义、避免虚函数表、提高缓存友好性等方面具有明显优势。熟练掌握 std::variant 的定义、访问和访客模式,可以让你在现代 C++ 开发中更加高效、可靠地处理“和而不同”的对象集合。希望这篇文章能帮助你快速上手,并在实际项目中灵活运用。