如何将 std::variant 的元素复制到另一个变体类型的变量

How to copy an element of std::variant to a variable of another variant-type

这是 的后续。 假设我们有两种具有部分相同成员类型的 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,会导致编译时错误。有解决办法吗?

上述示例不起作用的原因是 std::visit 要求为源 variant 的每个类型成员重载提交的功能对象的 operator()。但是对于其中一些类型,没有匹配的目标构造函数 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。它抛出了这个例子。或者它可以 return a std::monostate 如果它包含在目标 variant.

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

剩下的就是描述如何用struct split_types.

得到common_typessingle_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_lists to_tlfrom_tl。这些被转发给助手 split_types_h。这里两个空的 type_list 将被填充为 common 和 single 类型如下。

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.

转换函数可以如下使用(live demo)。

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';
}

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

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));
}

请注意,您需要明确提供 return 类型,否则在特殊情况下,它会被推断为 void 并且访问者不会都具有相同的 return类型。

使用 Exactly<U> 而不是 decltype(elem) 意味着将 variant<int> 转换为 variant<unsigned int> 将抛出错误而不是成功。如果打算让它成功,您可以使用 decltype(elem) 代替。


此处的替代方法是使用 Boost.Mp11,其中与模板元编程相关的所有内容都是 one-liner。这也是比较直接的检查:

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));
}

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

我们可以解决这个问题。

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(或其他可调用函数),returns 一个可调用函数,如果可能,它会分派给第一个,只有在第一个失败时才回退到第二个。