在现代 C++ 中,std::variant 成为一种强大且类型安全的替代传统 void* 或 union 的工具。它允许你在单个对象中存放多种类型中的一种,并在运行时通过访问器(std::get, std::visit 等)进行安全访问。下面将通过一个具体示例,演示如何利用 std::variant 构建一个简易的“多态容器”,并讨论其优点与使用注意事项。
1. 背景与需求
传统面向对象编程往往通过继承和虚函数实现多态,但在某些场景(如性能敏感、跨平台或非类类型)下,虚函数表(vtable)带来的开销和限制可能不太理想。C++17 引入的 std::variant 为此提供了一种轻量级、类型安全的方案。
我们需要实现一个容器 ShapeContainer,可以存放 Circle, Rectangle, Triangle 三种形状,并且能够对存放的形状执行对应的计算(面积、周长等),而无需依赖继承。
2. 代码实现
#include <iostream>
#include <variant>
#include <cmath>
#include <string>
#include <vector>
#include <optional>
// 形状结构体
struct Circle {
double radius;
};
struct Rectangle {
double width, height;
};
struct Triangle {
double a, b, c; // 三边长
};
// 计算圆面积
double area(const Circle& c) { return M_PI * c.radius * c.radius; }
double perimeter(const Circle& c) { return 2 * M_PI * c.radius; }
// 计算矩形面积
double area(const Rectangle& r) { return r.width * r.height; }
double perimeter(const Rectangle& r) { return 2 * (r.width + r.height); }
// 计算三角形面积(海伦公式)
double area(const Triangle& t) {
double s = (t.a + t.b + t.c) / 2.0;
return std::sqrt(s * (s - t.a) * (s - t.b) * (s - t.c));
}
double perimeter(const Triangle& t) { return t.a + t.b + t.c; }
// 定义 variant
using Shape = std::variant<Circle, Rectangle, Triangle>;
// 访问器函数
std::optional <double> shape_area(const Shape& s) {
return std::visit([](auto&& arg) -> double {
return area(arg);
}, s);
}
std::optional <double> shape_perimeter(const Shape& s) {
return std::visit([](auto&& arg) -> double {
return perimeter(arg);
}, s);
}
// 简易容器
class ShapeContainer {
public:
void add(const Shape& shape) { shapes_.push_back(shape); }
void print_all() const {
for (size_t i = 0; i < shapes_.size(); ++i) {
std::cout << "Shape #" << i << ":\n";
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, Circle>) {
std::cout << " Type: Circle, radius=" << arg.radius << "\n";
} else if constexpr (std::is_same_v<T, Rectangle>) {
std::cout << " Type: Rectangle, w=" << arg.width << ", h=" << arg.height << "\n";
} else if constexpr (std::is_same_v<T, Triangle>) {
std::cout << " Type: Triangle, a=" << arg.a << ", b=" << arg.b << ", c=" << arg.c << "\n";
}
std::cout << " Area: " << shape_area(shapes_[i]).value_or(0.0) << "\n";
std::cout << " Perimeter: " << shape_perimeter(shapes_[i]).value_or(0.0) << "\n";
}, shapes_[i]);
}
}
private:
std::vector <Shape> shapes_;
};
int main() {
ShapeContainer sc;
sc.add(Circle{5.0});
sc.add(Rectangle{4.0, 3.0});
sc.add(Triangle{3.0, 4.0, 5.0});
sc.print_all();
return 0;
}
关键点说明
- 类型安全:
std::variant的内部维护了类型信息,访问时不需要强制转换,编译器能检查类型匹配。 - 性能:
std::variant在多数实现中采用了小型对象优化(SBO),避免了堆分配。访问器std::visit通过模式匹配实现,在大多数情况下与传统虚函数调用相当甚至更快。 - 可组合:你可以用
std::variant与其他 STL 容器无缝组合(如上例的 `std::vector `)。
3. 使用场景与局限
| 场景 | 适用性 | 说明 |
|---|---|---|
| 需要在运行时选择多种具体实现 | ✔ | std::variant 适合有限的、已知类型集合 |
| 需要继承多态(动态类型绑定) | ❌ | 若类型列表可能无限扩展,或需要在运行时新增类型,传统继承更灵活 |
| 性能极端敏感(需要手动布局) | ❌ | 在极端低延迟或嵌入式场景,手写联合和分支可能更优 |
4. 小技巧
- 自定义
std::visit变体:如果你需要为variant自动生成多个访问器(如area,perimeter),可以用宏或模板元编程来减少重复代码。 - 错误处理:如果访问错误类型时想抛异常,可使用 `std::get (variant)` 或 `std::get_if`。
- 多语言互操作:当需要把
variant传递给 C 语言接口时,可将其拆成enum+union结构,保持 ABI 兼容。
5. 小结
std::variant 在 C++17 之后成为处理“有限多态”问题的首选工具。它兼具类型安全、易用性与高性能,适用于大多数需要在同一容器中存放不同类型数据的场景。通过本文示例,你可以快速上手并将 variant 集成到自己的项目中,替代传统虚表模式,实现更高效、可维护的代码架构。