# 使用 std::variant 与 std::visit 实现类型安全的多态容器

在 C++17 之后,std::variantstd::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::variantstd::visit 为 C++ 开发者提供了一种类型安全、性能高效的多态实现方案。通过它们可以在不牺牲性能的前提下,保持代码的可读性与可维护性。希望本文能帮助你在项目中更好地运用这两者,构建出更健壮、更高效的 C++ 代码。

发表评论