C++ 中的求和类型

Sum types in C++

在工作中,我 运行 遇到这样一种情况,其中描述函数返回结果的最佳类型是 std::variant<uint64_t, uint64_t> - 当然,这不是有效的 C++,因为您可以' 有两个相同类型的变体。我可以将其表示为 std::pair<bool, uint64_t>,或者该对的第一个元素是一个枚举,但这是一种特殊情况; std::variant<uint64_t, uint64_t, bool> 不能很好地表示,我的函数式编程背景确实让我想要 Either - 所以我尝试使用访客模式来实现它,就像我在其他模式中所做的那样不支持和类型的语言:

template <typename A, typename B, typename C>
class EitherVisitor {
    virtual C onLeft(const A& left) = 0;
    virtual C onRight(const B& right) = 0;
};

template <typename A, typename B>
class Either {
    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) = 0;
};

template <typename A, typename B>
class Left: Either<A, B> {
private:
    A value;
public:
    Left(const A& valueIn): value(valueIn) {}

    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) {
        return visitor.onLeft(value);
    }
};

template <typename A, typename B>
class Right: Either<A, B> {
private:
    B value;
public:
    Right(const B& valueIn): value(valueIn) {}
    
    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) {
        return visitor.onRight(value);
    }
};

C++ 拒绝这个,因为模板方法 Accept 不能是虚拟的。是否有针对此限制的变通方法,使我能够根据其 f 代数和变形正确地表示基本和类型?

std::variant<X,X> 是有效的 C++。

用起来有点别扭,因为std::visit不给你索引,std::get<X>也不行

解决这个问题的方法是创建一个变体索引,就像一个强大的枚举。

template<std::size_t i>
using index_t = std::integral_constant<std::size_t, i>;

template<std::size_t i>
constexpr index_t<i> index = {};

template<std::size_t...Is>
using number = std::variant< index_t<Is>... >;

namespace helpers {
  template<class X>
  struct number_helper;
  template<std::size_t...Is>
  struct number_helper<std::index_sequence<Is...>> {
    using type=number<Is...>;
  };
}
template<std::size_t N>
using alternative = typename helpers::number_helper<std::make_index_sequence<N>>::type;

然后我们可以从变体中提取备选方案:

namespace helpers {
  template<class...Ts, std::size_t...Is, class R=alternative<sizeof...(Ts)>>
  constexpr R get_alternative( std::variant<Ts...> const& v, std::index_sequence<Is...> ) {
    constexpr R retvals[] = {
      R(index<Is>)...
    };
    return retvals[v.index()];
  }
}
template<class...Ts>
constexpr alternative<sizeof...(Ts)> get_alternative( std::variant<Ts...> const& v )
{
  return helpers::get_alternative(v, std::make_index_sequence<sizeof...(Ts)>{});
}

所以现在你有一个 std::variant<int, int>,你可以

auto which = get_alternative( var );

which是一个变体,在运行时由一个整数表示,它是var中活动类型的索引。您可以:

std::variant<int, int> var( std::in_place_index_t<1>{}, 7 );

auto which = get_alternative( var );

std::visit( [&var](auto I) {
  std::cout << std::get<I>(var) << "\n";
}, get_alternative(var) );

并访问 var 中的哪些替代可能性是活跃的,具有编译时间常数。

我发现 get_alternative(variant) 使 variant<X,X,X> 更有用,并填补了我认为您可能会 运行 的漏洞。

Live example.

现在如果你不需要一个活跃的编译时索引,你可以调用var.index(),然后通过visit( lambda, var ).

访问

构造变体时,确实需要编译时索引来做variant<int, int> var( std::in_place_index_t<0>{}, 7 )。措辞 有点 尴尬,因为虽然 C++ 支持同一类型的倍数的变体,但它认为它们比通用代码之外的“标准”不相交变体更不可能。

但是我以前用过这个alternativeget_alternative之类的代码来支持像数据胶水代码这样的函数式编程。

也许最简单的解决方案是围绕 TRightLeft 进行轻量级包装? 基本上是一个强类型别名(也可以使用 Boost 的 strong typedef

template<class T>
struct Left
{
    T val;
};

template<class T>
struct Right
{
    T val;
};

然后我们可以区分它们进行访问:

template<class T, class U>
using Either = std::variant<Left<T>, Right<U>>;

Either<int, int> TrySomething()
{
    if (rand() % 2 == 0) // get off my case about rand(), I know it's bad
        return Left<int>{0};
    else
        return Right<int>{0};
}

struct visitor
{
    template<class T>
    void operator()(const Left<T>& val_wrapper)
    {
        std::cout << "Success! Value is: " << val_wrapper.val << std::endl;
    }
    
    template<class T>
    void operator()(const Right<T>& val_wrapper)
    {
        std::cout << "Failure! Value is: " << val_wrapper.val << std::endl;
    }
};

int main()
{
    visitor v;
    for (size_t i = 0; i < 10; ++i)
    {
        auto res = TrySomething();
        std::visit(v, res);
    }
}

Demo