【问题标题】:How to copy an element of std::variant to a variable of another variant-type如何将 std::variant 的元素复制到另一个变体类型的变量
【发布时间】:2019-05-21 21:26:24
【问题描述】:

这是对this answer 的跟进。 假设我们有两种类型的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,会导致编译时错误。有什么解决办法吗?

【问题讨论】:

    标签: c++ c++17 variant


    【解决方案1】:

    显然,要求是如果目标变体中不存在该类型,则抛出异常。我们可以通过引入一种只能完全转换为特定目标的新类型来做到这一点:

    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&lt;U&gt; 而不是仅仅使用decltype(elem) 意味着将variant&lt;int&gt; 转换为variant&lt;unsigned int&gt; 将抛出而不是成功。如果打算让它成功,您可以改用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));
    }
    

    【讨论】:

      【解决方案2】:

      上面的例子不起作用的原因是std::visit要求为源variant的每个类型成员重载提交的函数对象的operator()。但是对于其中一些类型,没有与目标 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 的成员。它在这个例子中抛出。或者,如果 std::monostate 包含在目标 variant 中,它可能会返回它。

      使用这些定义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_lists to_tlfrom_tl。这些被转发给助手split_types_h。这里将两个空的type_lists 填充为common 和single 类型,如下所示。

      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&lt;T2f,T2...&gt;) 的一个类型成员接在另一个之后,并检查目标是否也 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';
      }
      

      【讨论】:

      • 不错,但检测“可以转换为标准变体的类型”的方法非常复杂。
      【解决方案3】:

      您的问题是,并非源变体中的所有类型都由目标处理。

      我们可以解决这个问题。

      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 接受 2 个 lambdas(或其他可调用对象),并返回一个可调用对象,如果可能,该可调用对象将分派给第一个,如果第一个失败,则仅返回第二个。

      【讨论】:

      • 值得注意的是,这略有不同,因为它允许从 variant&lt;int&gt;variant&lt;unsigned int&gt; 的成功转换。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-04-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-12
      相关资源
      最近更新 更多