问题
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