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"));
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.
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"));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With