问题
This is a followup on this answer. Assume we have two types of std:variant with partly the same member types. For instance if we have
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_Day
is a sub-type of WeekDay
. Now how can we copy a variable of one type to a variable of the other type? If all type members of the source are type members of the target a conversion function can be defined as
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 ) );
}
It can be used as
Working_Day d1= Tuesday{};
WeekDay d2= var2var<WeekDay>( d1 );
Trying this the other way around, i.e. casting a WeekDay
into a Working_Day
, results in a compile time error. Is there any solution for this?
回答1:
Apparently the requirement is that if the type isn't present in the target variant, throw an exception. We can do that by introducing a new type which is only exactly convertible to a specific target:
template <typename T>
struct Exactly {
template <typename U, std::enable_if_t<std::is_same_v<T, U>, int> = 0>
operator U() const;
};
And then use that to either construct or throw:
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));
}
Note that you need to explicitly provide a return type because otherwise in the exceptional case, it would get deduced to void
and the visitors wouldn't all have the same return type.
The use of Exactly<U>
as opposed to just decltype(elem)
means that casting a variant<int>
to a variant<unsigned int>
will throw instead of succeeding. If the intend is to have it succeed, you can use decltype(elem)
instead.
An alternative here would be to use Boost.Mp11, in which everything template metaprogramming related is a one-liner. This is also a more direct check:
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));
}
回答2:
Your problem is that not all types in the source variant are handled by the destination.
We can fix this.
template<class...Fs>
struct overloaded : Fs... {
using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs&&...)->overloaded<std::decay_t<Fs>...>;
this is a helper that lets us pass around lambda or function overloads.
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 )
);
}
now that SFINAE is a mess. Let us hide it.
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
takes 2 lambdas (or other callables), and returns one a callable that dispatches to the first if possible, and only falls back on the second if the first fails.
回答3:
The reason why the example above does not work is that std::visit requires operator()
of the submitted functional object to be overloaded for each type member of the source variant
. But for some of these types there is no matching constructor of the target variant
.
The solution is to treat visiting differently for types which both variants
have in common and those which are members of the source variant
only.
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 ) );
}
Here std::visit
gets an object of struct conversion_visitor
. The latter takes template parameters common_types
and single_types
, which contain the type members of the source variant
split in the mentioned way.
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
is a container for types, which we use here because a variant
cannot be empty. conversion_visitor
is derived from structs gen_variant
and not_gen_variant
which both overload 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
is meant to treat the error cases, i.e. the cases in which the source contains a variable of a type which is not a member of the target variant
. It throws in this example. Alternatively it could return a std::monostate if that is contained in the target variant
.
With these definitions std::visit
will call conversion_visitor::operator()
. If the variable stored in the source has a type which the target can handle, that call is forwarded to gen_variant::operator()
. Otherwise it is forwarded to not_gen_variant::operator()
. gen_variant::operator()
just calls the constructor of the target variant
with the source element as argument.
What is left is to describe how to obtain common_types
and single_types
using struct 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
takes the target and the source variant
as template parameters. It first puts the members of those variants
into type_list
s to_tl
and from_tl
. These are forwarded to a helper split_types_h
. Here the two empty type_list
s will be filled up with the common and the single types as follows.
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
takes one type member of the source (type_list<T2f,T2...>
) after the other and checks if the target also contains
it. If so the type (T2f
) is appended to common_types
(with the help of c_types_h
). Otherwise it is appended to single_types
.
The casting function can be used as follows (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';
}
来源:https://stackoverflow.com/questions/56246573/how-to-copy-an-element-of-stdvariant-to-a-variable-of-another-variant-type