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