Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++17

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"));
like image 919
Alexey Starinsky Avatar asked Jun 26 '19 14:06

Alexey Starinsky


2 Answers

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.

like image 117
Barry Avatar answered Sep 24 '22 13:09

Barry


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"));
like image 44
Holt Avatar answered Sep 26 '22 13:09

Holt