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

4
这是对这个答案的跟进。假设我们有两种带有部分相同成员类型的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个回答

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
上面的例子不起作用的原因是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,则可以返回该值。

使用这些定义,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_listto_tlfrom_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...>)的每个成员,并检查目标类型是否也包含它。如果是,则使用c_types_h的帮助将该类型(T2f)添加到common_types中。否则,它将被添加到single_types中。

转换函数可以按以下方式使用(实时演示)。

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

1
不错,但是检测“可以转换为std variant的类型”的方法非常复杂。 - Yakk - Adam Nevraumont

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接受两个lambda表达式(或其他可调用对象),并返回一个可调用对象,如果可能的话将分派到第一个,只有在第一个失败时才会回退到第二个。


值得注意的是,这里略有不同,它允许从例如 variant<int>variant<unsigned int> 的成功转换。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接