在 C++17 之后,std::variant 和 std::visit 成为处理多种类型的强大工具。相比传统的继承与虚函数,variant 在编译时完成类型检查,避免了运行时错误;相比 boost::variant,它是标准库的一部分,完全跨平台。本文将介绍 std::variant 的基本使用、访问方法以及 std::visit 的实现原理,并给出一个实用的多态容器示例。
1. std::variant 的基本概念
std::variant<Types...> 是一个联合体类型,内部只能存储 Types... 中的一种类型。它类似于 boost::variant 或者 C# 的 object,但具有强类型安全:
std::variant<int, std::string, double> v;
v = 42; // 存储 int
v = std::string("hello"); // 存储 std::string
1.1 访问方式
- **`std::get (v)`**:若 `v` 存储的是类型 `T`,返回其引用;否则抛出 `std::bad_variant_access`。
- **`std::get_if (&v)`**:若 `v` 存储的是类型 `T`,返回指针,否则返回 `nullptr`。
std::get<std::size_t>(v):根据索引访问内部存储的类型。
2. std::visit 的工作原理
std::visit 是一个高阶函数,用来访问 variant 的值,并通过可调用对象(如 lambda、函数对象)实现不同类型的处理。它会根据 variant 当前存储的类型,选择对应的调用:
std::visit([](auto&& arg) {
// 这里 arg 的类型由 variant 自动推断
std::cout << arg << '\n';
}, v);
内部实现类似于多重模板展开,利用 C++17 的 if constexpr 或者 switch 语句。编译器会为每种可能的类型生成对应的代码路径,从而保证高效。
3. 实例:一个图形对象容器
假设我们需要处理三种几何图形:圆、矩形和三角形。传统的做法是定义一个基类 Shape 并通过虚函数实现多态;但这里我们用 std::variant 取代基类,展示其优势。
#include <variant>
#include <iostream>
#include <string>
#include <cmath>
struct Circle {
double radius;
};
struct Rectangle {
double width;
double height;
};
struct Triangle {
double a, b, c; // 三边
};
using Shape = std::variant<Circle, Rectangle, Triangle>;
// 计算面积的通用函数
double area(const Shape& shape) {
return std::visit([](auto&& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Circle>) {
return M_PI * s.radius * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return s.width * s.height;
} else if constexpr (std::is_same_v<T, Triangle>) {
// 海伦公式
double p = (s.a + s.b + s.c) / 2.0;
return std::sqrt(p * (p - s.a) * (p - s.b) * (p - s.c));
} else {
static_assert(always_false <T>::value, "non-exhaustive visitor!");
}
}, shape);
}
// 计算周长的通用函数
double perimeter(const Shape& shape) {
return std::visit([](auto&& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Circle>) {
return 2.0 * M_PI * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return 2.0 * (s.width + s.height);
} else if constexpr (std::is_same_v<T, Triangle>) {
return s.a + s.b + s.c;
} else {
static_assert(always_false <T>::value, "non-exhaustive visitor!");
}
}, shape);
}
// 辅助模板用于 static_assert
template <class> struct always_false : std::false_type {};
3.1 说明
std::visit通过if constexpr判断当前存储的类型,并执行对应的计算逻辑。always_false用于在缺失某种类型时触发编译错误,确保访问函数覆盖所有可能的variant成员。- 由于所有计算都在模板展开期间完成,运行时开销极低。
4. 与传统多态的比较
| 维度 | 基类+虚函数 | std::variant + std::visit |
|---|---|---|
| 运行时多态 | 通过 vtable | 无 vtable,直接调用模板代码 |
| 编译时类型安全 | 需要 RTTI 或 dynamic_cast |
static_assert + if constexpr |
| 可扩展性 | 需要修改基类 | 只需在 variant 里添加新类型即可 |
| 性能 | 运行时 dispatch | 编译时展开,无额外 indirection |
| 内存布局 | 对象表 | 内部 union + index,紧凑 |
5. 何时使用 std::variant
- 值语义:需要以值传递而非引用或指针。
- 类型组合有限:类型集合已知且数量有限。
- 避免多态带来的开销:在性能敏感的代码中。
- 需要类型安全的错误检查:
std::variant可在编译期捕捉错误。
6. 小结
std::variant 与 std::visit 为 C++ 开发者提供了一种类型安全、性能高效的多态实现方案。通过它们可以在不牺牲性能的前提下,保持代码的可读性与可维护性。希望本文能帮助你在项目中更好地运用这两者,构建出更健壮、更高效的 C++ 代码。