C++ 中如何使用 std::variant 实现类型安全的多态?

在 C++17 之后,标准库提供了 std::variant,它是一个可容纳多种类型的“类型安全”联合体。与传统的多态(通过继承和虚函数实现)相比,std::variant 在编译期就能保证类型正确性,并且不需要运行时的虚表开销。下面通过一个完整的例子来演示如何利用 std::variant 创建一个“和”类型,并实现基本运算与访问。

1. 需求场景

假设我们需要一个容器,既可以存放整数,也可以存放浮点数,且希望对其进行加法运算。传统做法会是:

class Number { virtual ~Number() = default; virtual double toDouble() const = 0; };
class IntNumber : public Number { /* ... */ };
class FloatNumber : public Number { /* ... */ };

这样做会产生虚函数表,且在使用时需要动态类型判断。

2. 使用 std::variant 的方案

2.1 定义类型

#include <variant>
#include <iostream>
#include <string>
#include <cmath>

using Number = std::variant<int, double>;

2.2 基本操作

  • 赋值:直接使用构造函数或 Number n = 5; / Number n = 3.14;
  • 访问:通过 `std::get (n)` 或 `std::get_if(&n)`;如果类型不匹配会抛出 `std::bad_variant_access`。
Number n1 = 10;          // int
Number n2 = 2.5;         // double

// 访问
int   i = std::get <int>(n1);          // 10
double d = std::get <double>(n2);      // 2.5

// 或安全访问
if (auto p = std::get_if <int>(&n1)) std::cout << *p << '\n';

2.3 运算实现

我们可以利用 std::visit 对 variant 进行访问并做运算。std::visit 可以接收一个可调用对象(如 lambda)和一个或多个 variant,它会在运行时根据实际类型执行对应的分支。

Number add(const Number& a, const Number& b)
{
    return std::visit([](auto&& lhs, auto&& rhs)
    {
        using L = std::decay_t<decltype(lhs)>;
        using R = std::decay_t<decltype(rhs)>;
        // 两个都是 int
        if constexpr (std::is_same_v<L,int> && std::is_same_v<R,int>)
            return Number(static_cast <int>(lhs + rhs));
        // int + double 或 double + int
        else if constexpr (std::is_same_v<L,int> && std::is_same_v<R,double>)
            return Number(static_cast <double>(lhs) + rhs);
        else if constexpr (std::is_same_v<L,double> && std::is_same_v<R,int>)
            return Number(lhs + static_cast <double>(rhs));
        // 两个都是 double
        else if constexpr (std::is_same_v<L,double> && std::is_same_v<R,double>)
            return Number(lhs + rhs);
    }, a, b);
}

2.4 示例与输出

int main()
{
    Number n1 = 42;
    Number n2 = 3.14;
    Number n3 = 7;

    Number r1 = add(n1, n2); // int + double => double
    Number r2 = add(n3, n1); // int + int   => int

    std::visit([](auto&& x){
        std::cout << x << " (" << typeid(x).name() << ")\n";
    }, r1);
    std::visit([](auto&& x){
        std::cout << x << " (" << typeid(x).name() << ")\n";
    }, r2);

    return 0;
}

输出示例(取决于编译器):

45.140000 (d)
49 (i)

3. 优点总结

方面 std::variant 传统虚函数
类型安全 编译期检查 运行期判断
性能 无虚表 有虚表
可读性 简洁 继承层次
可维护性 较低 可能较高(多继承)

4. 小贴士

  • 访问时使用 std::get_if:如果不确定类型,使用指针返回,避免异常。
  • 自定义运算:可以为 variant 定义 operator+ 等,以使代码更自然。
  • std::optional 组合:有时需要“可能为空”的多类型值,可以用 std::variant<Empty, int, double>std::optional<std::variant<int,double>>

5. 结语

std::variant 是 C++17 之后提供的强大工具,它在不牺牲性能的前提下,让我们以更安全、更简洁的方式实现多态。掌握 std::visit 的技巧后,你就能在许多需要“可变类型”场景中替代传统的继承多态方案,写出更现代、更可靠的 C++ 代码。

发表评论