如何将std :: variant的元素复制到另一个variant-type的变量

时间:2019-05-21 21:26:25

标签: c++ c++17 variant

这是对this answer的跟进。 假设我们有两种std:variant,成员类型部分相同。例如,如果我们有

struct Monday {};
struct Tuesday {};
/* ... etc. */
using WeekDay= std::variant<Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday>;
using Working_Day= std::variant<Monday, Tuesday, Wednesday, Thursday, Friday>;

Working_DayWeekDay的子类型。现在我们如何将一种类型的变量复制到另一种类型的变量?如果源的所有类型成员都是目标的类型成员,则可以将转换函数定义为

template <typename To, typename From>
To var2var( From && from )
{
    return std::visit(
        []( auto && elem ) { return To( std::forward<decltype(elem)>( elem ) ); },
        std::forward<From>( from ) );
}

它可以用作

Working_Day  d1= Tuesday{};
WeekDay      d2= var2var<WeekDay>( d1 );

以另一种方式尝试,即将WeekDay转换为Working_Day会导致编译时错误。有什么解决办法吗?

3 个答案:

答案 0 :(得分:2)

显然,要求是如果目标变体中不存在该类型,则引发异常。我们可以通过引入只能精确地转换为特定目标的新类型来做到这一点:

template <typename T>
struct Exactly {
    template <typename U, std::enable_if_t<std::is_same_v<T, U>, int> = 0>
    operator U() const;
};

然后使用它来构造或抛出:

template <typename To, typename From>
To unsafe_variant_cast(From && from)
{
    return std::visit([](auto&& elem) -> To {
        using U = std::decay_t<decltype(elem)>;
        if constexpr (std::is_constructible_v<To, Exactly<U>>) {
            return To(std::forward<decltype(elem)>(elem));
        } else {
            throw std::runtime_error("Bad type");
        }
    }, std::forward<From>(from));
}

请注意,您需要显式提供返回类型,因为在特殊情况下,它会被推导为void,并且访问者不会都具有相同的返回类型。

使用Exactly<U>而不是decltype(elem)意味着将variant<int>投射到variant<unsigned int>会抛出而不是成功。如果打算使它成功,则可以改用decltype(elem)


这里的一种替代方法是使用Boost.Mp11,其中与模板元编程相关的所有内容都是单一的。这也是更直接的检查:

template <typename To, typename From>
To unsafe_variant_cast(From && from)
{
    return std::visit([](auto&& elem) -> To {
        using U = std::decay_t<decltype(elem)>;
        if constexpr (mp_contains<To, U>::value) {
            return To(std::forward<decltype(elem)>(elem));
        } else {
            throw std::runtime_error("Bad type");
        }
    }, std::forward<From>(from));
}

答案 1 :(得分:1)

您的问题是,源变体中并非所有类型都由目标处理。

我们可以解决这个问题。

template<class...Fs>
struct overloaded : Fs... {
  using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs&&...)->overloaded<std::decay_t<Fs>...>;

这是一个帮助程序,可以让我们传递lambda或函数重载。

template<class To, class From>
To var2var( From && from )
{
  return std::visit(
    overloaded{
      []( To elem ) { return elem; },
      []( auto&& x )
      ->std::enable_if_t< !std::is_convertible<decltype(x), To>{}, To> {
        throw std::runtime_error("wrong type");
      }
    },
    std::forward<From>( from )
  );
}

现在SFINAE一片混乱。让我们隐藏它。

template<class F, class Otherwise>
auto call_or_otherwise( F&& f, Otherwise&& o ) {
  return overloaded{
    std::forward<F>(f),
    [o = std::forward<Otherwise>(o)](auto&&... args)
    -> std::enable_if_t< !std::is_invocable< F&, decltype(args)... >{}, std::invoke_result< Otherwise const&, decltype(args)... > >
    { return o( decltype(args)(args)... ); }
  };
}

template<class To, class From>
To var2var( From && from )
{
  return std::visit(
    call_or_otherwise(
        [](To to){ return to; },
        [](auto&&)->To{ throw std::runtime_error("type mismatch"); }
    ),
    std::forward<From>(from)
  );
}

call_or_otherwise占用2个lambda(或其他可调用对象),并向其中一个调用对象返回一个可调用对象,该可调用对象在可能的情况下分派给第一个,而在第一个失败时仅回退到第二个。

答案 2 :(得分:0)

上面的示例不起作用的原因是std::visit要求为源operator()的每个类型成员重载variant的已提交功能对象。但是对于其中某些类型,没有目标variant的匹配构造函数。

解决方案是对variants共同的类型和仅属于源variant的类型的访问进行区别对待。

template <class To, class From>
To var2var( From && from ) 
{
    using FRM= std::remove_reference_t<From>;
    using TO=  std::remove_reference_t<To>;
    using common_types= typename split_types<TO, FRM>::common_types;
    using single_types= typename split_types<TO, FRM>::single_types;
    return std::visit(
        conversion_visitor<TO, common_types, single_types>(),
        std::forward<From>( from ) );
}

此处std::visit获取struct conversion_visitor的对象。后者采用模板参数common_typessingle_types,它们包含以上述方式拆分的源variant的类型成员。

template<class... T> struct type_list {};

template <class To, class V1, class V2>
struct conversion_visitor;

template <class To, class... CT, class... ST>
struct conversion_visitor< To, type_list<CT...>, type_list<ST...> > 
: public gen_variant<To, CT>...
, public not_gen_variant<To, ST>...
{
    using gen_variant<To,CT>::operator()...;
    using not_gen_variant<To,ST>::operator()...;
};

type_list是类型的容器,我们在这里使用它是因为variant不能为空。 conversion_visitor源自结构gen_variantnot_gen_variant,它们都重载operator()

template<class To, class T>
struct gen_variant
{
    To operator()( T const & elem ) { return To( elem ); }
    To operator()( T && elem ) { return To( std::forward<T>( elem ) ); }
};

template<class To, class T>
struct not_gen_variant
{
    To operator()( T const & ) { throw std::runtime_error("Type of element in source variant is no type member of target variant"); }
};

not_gen_variant用于处理错误情况,即,源包含不属于目标variant成员的类型的变量的情况。它抛出了这个例子。或者,如果目标variant中包含std::monostate,则它可以返回live demo

使用这些定义,std::visit将调用conversion_visitor::operator()。如果源中存储的变量具有目标可以处理的类型,则该调用将转发到gen_variant::operator()。否则,它将转发到not_gen_variant::operator()gen_variant::operator()仅使用源元素作为参数调用目标variant的构造函数。

剩下的是描述如何使用common_types获取single_typesstruct split_types

template<class T1, class T2>
struct split_types;

template<class... To, class... From>
struct split_types< std::variant<To...>, std::variant<From...> >
{
    using to_tl=   type_list<std::remove_reference_t<To>...>;
    using from_tl= type_list<std::remove_reference_t<From>...>;
    using common_types= typename split_types_h<to_tl, from_tl, type_list<>, type_list<> >::common_types;
    using single_types= typename split_types_h<to_tl, from_tl, type_list<>, type_list<> >::single_types;
};

split_types将目标variant和源variants作为模板参数。首先,将这些type_list的成员放入to_tl的{​​{1}}和from_tl中。这些将转发给助手split_types_h。在这里,两个空的type_list将被常见和单个类型填充,如下所示。

template<class T1, class T2, bool>
struct append_if;

template<class... Ts, class T>
struct append_if< type_list<Ts...>, T, true >
{
  using type= type_list< Ts..., T >;
};

template<class... Ts, class T>
struct append_if< type_list<Ts...>, T, false >
{
  using type= type_list< Ts... >;
};

template<class T1, class T2, bool b>
using append_if_t= typename append_if<T1, T2, b>::type;


template<class T1, class T2, class CT, class ST >
struct split_types_h;

template<class... T1, class... CT, class... ST>
struct split_types_h< type_list<T1...>, type_list<>, type_list<CT...>, type_list<ST...> >
{
    using common_types= type_list<CT...>;
    using single_types= type_list<ST...>;
};

template<class... T1, class T2f, class... T2, class... CT, class... ST>
struct split_types_h< type_list<T1...>, type_list<T2f,T2...>, type_list<CT...>, type_list<ST...> >
{
    enum : bool { contains= (std::is_same_v<T2f,T1> || ...) };
    using c_types_h= append_if_t<type_list<CT...>, T2f,  contains>;
    using s_types_h= append_if_t<type_list<ST...>, T2f, !contains>;
    using common_types= typename split_types_h<type_list<T1...>, type_list<T2...>, c_types_h, s_types_h>::common_types;
    using single_types= typename split_types_h<type_list<T1...>, type_list<T2...>, c_types_h, s_types_h>::single_types;
};

split_types_h将源的一种类型成员(type_list<T2f,T2...>)紧随其后,并检查目标是否也contains。如果是这样,则在T2f的帮助下,将类型(common_types)附加到c_types_h。否则,它将附加到single_types

强制转换功能可以按以下方式使用(the documentation)。

Working_Day  d1= Tuesday{};
Working_Day  d2= d1;
WeekDay      d3= Saturday{};

d3= var2var<WeekDay>( d1 );
d2= var2var<Working_Day>( d3 );
d2= var2var<Working_Day>( d1 );
try
{
    WeekDay d4= Sunday{};
    d1= var2var<Working_Day>( d4 );
}
catch( std::runtime_error & err )
{
    std::cerr << "Runtime error caught: " << err.what() << '\n';
}