C中的union在C++中一直没有很好的替代方案,直到C++17引入了std::variant

std::variant的用法到处都查得到,我不想写废话。不过每次使用都要手写一个class visitor实在是太智障了,这里参照cppreference的示例写个方便使用的Match:

template<typename...Ts>
using Variant = std::variant<Ts...>;
 
template<typename E, typename...Vs>
auto MatchVar(E &&e, Vs...vs)
{
    struct overloaded : Vs...
    {
        explicit overloaded(Vs...vss) : Vs(vss)... { }
    };
    return std::visit(overloaded(vs...), std::forward<E>(e));
}

用法是这样的:

Variant<int, float, std::string> tu = 5
auto v = MatchVar(tu,
    [](float f) { return f;        },
    [](int x)   { return x + 2.0f; },
    [](auto v)  { return 0.0f;     });
assert(ApproxEq(v, 7.0f));

有点像Rust的enum,只不过不允许手动指定Tag,只能拿类型当标记。现在稍微解析一下那一小段代码的原理。

MatchVar的第一个模板参数就是Variant的类型,无需多言。后面的变长模板参数则是针对不同类型的值做不同处理的可调用对象的类型。在内部,我定义了一个overloaded类,它继承了这些对象的类型,也就是说这些对象都地是类而不是函数指针之类的东西。overloaded在继承Vs...的同时也继承了它们的operator()方法,它的构造函数则把这一大堆Vs基类初始化。

最后就是用Vs...构造一个overloaded,再将它传递给std::visit了,没什么好说的。

使用时需要注意的有两点:

  1. 每个处理对象的返回值类型必须相同,不然通不过编译;
  2. 处理对象的参数类型必须覆盖Variant包含的所有类型,否则会爆出完全看不懂的编译错误。可以用auto参数类型的lambda来处理缺省分支,就像Rust中的_