4

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?

Claas Bontus
  • 1,628
  • 1
  • 15
  • 29

3 Answers3

2

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));
}
Barry
  • 286,269
  • 29
  • 621
  • 977
1

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_lists to_tl and from_tl. These are forwarded to a helper split_types_h. Here the two empty type_lists 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';
}
Claas Bontus
  • 1,628
  • 1
  • 15
  • 29
1

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Worth noting that is slightly different as it allows a successful cast from, e.g., `variant` to `variant`. – Barry May 21 '19 at 22:46