Get a std::tuple element as std::variant

∥☆過路亽.° 提交于 2020-01-24 05:11:04

问题


Given a variant type:

using Variant = std::variant<bool, char, int, float, double, std::string>;

and a tuple type containing elements restricted to this variant types (duplicates and omissions are possible, but no additional types):

using Tuple = std::tuple<char, int, int, double, std::string>;

How to implement methods that gets and sets a tuple element by a given index as Variant at runtime:

Variant Get(const Tuple & val, size_t index);
void Set(Tuple & val, size_t index, const Variant & elem_v);

I have two implementations in my code, but I have an impression that there can be a better one. My first implementation uses std::function and the second builds an array of some Accessor pointers that imposes restrictions on moving and copying my object (because its address changes). I wonder if someone knows the right way to implement this.

EDIT1:

The following example probably clarifies what I mean:

Tuple t = std::make_tuple(1, 2, 3, 5.0 "abc");
Variant v = Get(t, 1);
assert(std::get<int>(v) == 2);
Set(t, 5, Variant("xyz"));
assert(std::get<5>(t) == std::string("xyz"));

回答1:


I'm going to continue my theme of recommending Boost.Mp11 for all metaprogramming things, because there is always a function for that. In this case, we want mp_with_index. That function lifts a runtime index into a compile-time index.

Variant Get(Tuple const& val, size_t index)
{
    return mp_with_index<std::tuple_size_v<Tuple>>(
        index,
        [&](auto I){ return Variant(std::get<I>(val)); }
        );
}

Given that in the OP, the indices of the Tuple and the Variant don't even line up, the Set needs to actually visit the Variant rather than relying on the index. I'm using is_assignable here as the constraint, but that can be adjusted to as fitting for the problem (e.g. maybe it should be is_same).

void Set(Tuple& val, size_t index, Variant const& elem_v)
{
    mp_with_index<std::tuple_size_v<Tuple>>(
        index,
        [&](auto I){
            std::visit([&](auto const& alt){
                if constexpr (std::is_assignable_v<
                        std::tuple_element_t<Tuple, I>,
                        decltype(alt)>)
                {
                    std::get<I>(val) = alt;
                } else {
                    throw /* something */;
                }
            }, elem_v);
        });
}

If you require that every type in the Tuple appears exactly once in the Variant, and you want to directly only assign from that type without doing any conversions, this can be simplified to:

void Set(Tuple& val, size_t index, Variant const& elem_v)
{
    mp_with_index<std::tuple_size_v<Tuple>>(
        index,
        [&](auto I){
            using T = std::tuple_element_t<Tuple, I>;
            std::get<I>(val) = std::get<T>(elem_v);
        });
}

which will throw if the variant is not engaged with that type.




回答2:


Here are possible implementations of a get_runtime and set_runtime functions that rely on recursion to try to match the runtime index to a compile time one:

template <class Variant, class Tuple, std::size_t Index = 0>
Variant get_runtime(Tuple &&tuple, std::size_t index) {
    if constexpr (Index == std::tuple_size_v<std::decay_t<Tuple>>) {
        throw "Index out of range for tuple";
    }
    else {
        if (index == Index) {
            return Variant{std::get<Index>(tuple)};
        }
        return get_runtime<Variant, Tuple, Index + 1>(
            std::forward<Tuple>(tuple), index);
    }
}


template <class Tuple, class Variant, std::size_t Index = 0>
void set_runtime(Tuple &tuple, std::size_t index, Variant const& variant) {
    if constexpr (Index == std::tuple_size_v<std::decay_t<Tuple>>) {
        throw "Index out of range for tuple";
    }
    else {
        if (index == Index) {
            // Note: You should check here that variant holds the correct type
            // before assigning.
            std::get<Index>(tuple) = 
                std::get<std::tuple_element_t<Index, Tuple>>(variant);
        }
        else {
            set_runtime<Tuple, Variant, Index + 1>(tuple, index, variant);
        }
    }
}

You can use them like your Get and Set:

using Variant = std::variant<bool, char, int, float, double, std::string>;
using Tuple = std::tuple<char, int, int, double, std::string>;

Tuple t = std::make_tuple(1, 2, 3, 5.0, "abc");
Variant v = get_runtime<Variant>(t, 1);
assert(std::get<int>(v) == 2);
set_runtime(t, 4, Variant("xyz"));
assert(std::get<4>(t) == std::string("xyz"));



回答3:


template <size_t... I>
Variant GetHelper(const Tuple& val, size_t index, std::index_sequence<I...>)
{
    Variant value;
    int temp[] = {
        ([&]
        {
            if (index == I)
                value = std::get<I>(val);
        }(), 0)... };
    return value;
}

Variant Get(const Tuple& val, size_t index)
{
    return GetHelper(val, index, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}

template <size_t... I>
void SetHelper(Tuple& val, size_t index, Variant elem_v, std::index_sequence<I...>)
{
    int temp[] = {
        ([&]
        {
            using type = std::tuple_element_t<I, Tuple>;
            if (index == I)
                std::get<I>(val) = std::get<type>(elem_v);
        }(), 0)... };
}

void Set(Tuple& val, size_t index, Variant elem_v)
{
    SetHelper(val, index, elem_v, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}

Explanation:

Use std::index_sequence to get access to every tuple element via compile time constant index I. Create a lambda for each index, that performs the desired action if the index matches, and call it immediately (note the () right after the lambdas). Use the syntax int temp[] = { (some_void_func(), 0)... } to actually call every lambda (you cannot use the unpacking syntax ... on void functions directly, hence this trick to assign it to an int array).

Alternatively, you can make your lambdas return some dummy int. Then you can call them via unpacking directly.




回答4:


First, some machinery.

alternative is a variant of integral constants, which are stateless. We can then use visit on them to convert a bounded runtime value to a compile time value.

template<class T, T...Is>
using alternative = std::variant< std::integral_constant<T, Is>... >;

template<class List>
struct alternative_from_sequence;
template<class T, T...Is>
struct alternative_from_sequence< std::integer_sequence<T,Is...> > {
  using type=alternative<T, Is...>;
};
template<class T, T Max>
using make_alternative = typename alternative_from_sequence<
  std::make_integer_sequence<T, Max>
>::type;

template<class T, T Max, T Cur=Max-1>
make_alternative<T, Max> get_alternative( T value, std::integral_constant< T, Max > ={} ) {
  if(Cur == 0 || value == Cur) {
    return std::integral_constant<T, Cur>{};
  }
  if constexpr (Cur > 0)
  {
    return get_alternative<T, Max, Cur-1>( value );
  }
}
template<class...Ts>
auto get_alternative( std::variant<Ts...> const& v ) {
    return get_alternative<std::size_t, sizeof...(Ts) >( v.index() );
}

now your actual problem. This Get requires you pass your Variant type:

template<class Variant, class...Ts>
Variant Get(std::tuple<Ts...> const & val, size_t index) {
  auto which = get_alternative<std::size_t, sizeof...(Ts)>( index );
  return std::visit( [&val]( auto i )->Variant {
    return std::get<i>(val);
  }, which );
}

Your Set function seems toxic; if the types don't match, there is no practical recourse. I'll add a return value that states if the assignment failed:

template<class...Ts, class...Vs>
bool Set(
  std::tuple<Ts...> & val,
  std::size_t index,
  const std::variant<Vs...>& elem_v
) {
  auto tuple_which = get_alternative<std::size_t, sizeof...(Ts)>( index );
  auto variant_which = get_alternative( elem_v );
  return std::visit( [&val, &elem_v](auto tuple_i, auto variant_i) {
    using variant_type = std::variant_alternative_t<variant_i, std::variant<Vs...>>;
    using tuple_type = std::tuple_element_t< tuple_i, std::tuple<Ts...> >;

    if constexpr (!std::is_assignable<tuple_type&, variant_type const&>{}) {
      return false;
    } else {
      std::get<tuple_i>(val) = std::get<variant_i>(elem_v);
      return true;
    }
  }, tuple_which, variant_which );
}

This Set returns false if the types are not assignable.

Live example.



来源:https://stackoverflow.com/questions/56775116/get-a-stdtuple-element-as-stdvariant

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!