Как скопировать элемент std :: variable в переменную другого варианта - PullRequest
2 голосов
/ 22 мая 2019

Это продолжение этого ответа . Предположим, у нас есть два типа 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_Day является подтипом WeekDay. Теперь, как мы можем скопировать переменную одного типа в переменную другого типа? Если все члены типа источника являются членами типа цели, функцию преобразования можно определить как

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 голосов
/ 22 мая 2019

Очевидно, что требование состоит в том, что если тип отсутствует в целевом варианте, выдается исключение.Мы можем сделать это, введя новый тип, который только точно может быть преобразован в конкретную цель:

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 голос
/ 22 мая 2019

Ваша проблема в том, что не все типы в исходном варианте обрабатываются адресатом.

Мы можем это исправить.

template<class...Fs>
struct overloaded : Fs... {
  using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs&&...)->overloaded<std::decay_t<Fs>...>;

это помощник, который позволяет нам обходить лямбда или перегрузки функций.

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 лямбды (или другие вызываемые функции) и возвращает одну вызываемую, которая отправляется первому, если это возможно, и откатывается на вторую только в случае неудачи первого.

0 голосов
/ 22 мая 2019

Причина, по которой вышеприведенный пример не работает, заключается в том, что 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_types и single_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_variant и not_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. Это бросает в этом примере. В качестве альтернативы он может вернуть std::monostate, если он содержится в цели variant.

С этими определениями std::visit будет вызывать conversion_visitor::operator(). Если переменная, хранящаяся в источнике, имеет тип, который может обрабатывать цель, этот вызов перенаправляется на gen_variant::operator(). В противном случае он передается на not_gen_variant::operator(). gen_variant::operator() просто вызывает конструктор цели variant с исходным элементом в качестве аргумента.

Осталось описать, как получить common_types и single_types, используя 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 принимает цель и источник variant в качестве параметров шаблона. Сначала он помещает членов этих variants в type_list s to_tl и from_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...>) за другим и проверяет, если цель также 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';
}
...