What is the shortest / best way to replace the n-th element of a tuple with a value (which may or may not have a different type)? Solutions including c++20 are fine. [EDIT: I would prefer something not requiring other libraries, but I'm still interested what solutions are possible with e.g. boost].
I.e.:
#include <cassert>
#include <tuple>
template<std::size_t N, ... >
auto replace_tuple_element( ... ) // <- Looking for a suitable implementation
struct Foo {
int value;
};
int main()
{
auto t1 = std::tuple{ 0, 1, 2, 3 };
auto t2 = replace_tuple_element<2>( t1, Foo{10} );
assert( std::get<0>(t2) == std::get<0>(t1));
assert( std::get<1>(t2) == std::get<1>(t1));
assert( std::get<2>(t2).value == 10);
assert( std::get<3>(t2) == std::get<3>(t1));
}
Note: Just replacing the n-th type in a typelist has e.g. be discussed here: How do I replace a tuple element at compile time?. But I also want to replace the value and hope that there are simpler/more elegant solutions now in c++20 than back when that question was asked.
Tuples in C++. 1. get() :- get() is used to access the tuple values and modify them, it accepts the index and tuple name as arguments to access a particular tuple element. 2. make_tuple() :- make_tuple() is used to assign tuple with values. The values passed should be in order with the values declared in tuple.
A tuple is an object that can hold a number of elements. The elements can be of different data types. The elements of tuples are initialized as arguments in order in which they will be accessed.
But there is a workaround. You can convert the tuple into a list, change the list, and convert the list back into a tuple.
One can get an element from std::tupleby index using std::get. Analogically, how to settuple's element by index? c++templatesindexingtuples
One solution I found for c++20 is this:
#include <cassert>
#include <tuple>
#include <type_traits>
template<std::size_t N, class TupleT, class NewT>
constexpr auto replace_tuple_element( const TupleT& t, const NewT& n )
{
constexpr auto tail_size = std::tuple_size<TupleT>::value - N - 1;
return [&]<std::size_t... I_head, std::size_t... I_tail>
( std::index_sequence<I_head...>, std::index_sequence<I_tail...> )
{
return std::tuple{
std::get<I_head>( t )...,
n,
std::get<I_tail + N + 1>( t )...
};
}(
std::make_index_sequence<N>{},
std::make_index_sequence<tail_size>{}
);
}
struct Foo {
int value;
};
int main()
{
auto t1 = std::tuple{ 0, 1, 2, 3 };
auto t2 = replace_tuple_element<2>( t1, Foo{10} );
assert( std::get<0>(t2) == std::get<0>(t1));
assert( std::get<1>(t2) == std::get<1>(t1));
assert( std::get<2>(t2).value == 10);
assert( std::get<3>(t2) == std::get<3>(t1));
}
What I like about the solution is that it is a single, self containied function. I wonder if there is something even shorter and/or more readable though.
Possible solution:
template<std::size_t i>
using index = std::integral_constant<std::size_t, i>;
template<std::size_t N, class Tuple, typename S>
auto replace_tuple_element(Tuple&& tuple, S&& s) {
auto get_element = [&tuple, &s]<std::size_t i>(Index<i>) {
if constexpr (i == N)
return std::forward<S>(s);
else
return std::get<i>(std::forward<Tuple>(tuple));
};
using T = std::remove_reference_t<Tuple>;
return [&get_element]<std::size_t... is>(std::index_sequence<is...>) {
return std::make_tuple(get_element(index<is>{})...);
}(std::make_index_sequence<std::tuple_size_v<T>>{});
}
Note this decays all element types, removing references and const.
This amendment partially addresses this issue:
template<std::size_t N, class Tuple, typename S>
auto replace_tuple_element(Tuple&& tuple, S&& s) {
using T = std::remove_reference_t<Tuple>;
auto get_element = [&tuple, &s]<std::size_t i>(index<i>) {
if constexpr (i == N)
return std::forward<S>(s);
else
if constexpr (std::is_lvalue_reference_v<std::tuple_element_t<i, T>>)
return std::ref(std::get<i>(std::forward<Tuple>(tuple)));
else
return std::get<i>(std::forward<Tuple>(tuple));
};
return [&get_element]<std::size_t... is>(std::index_sequence<is...>) {
return std::make_tuple(get_element(index<is>{})...);
}(std::make_index_sequence<std::tuple_size_v<T>>{});
}
Now replace_tuple_element
also follows the convention of std::make_tuple
that converts std::reference_wrapper
arguments into references. It does preserve reference types, but drops top-level constness.
struct Foo {
Foo(int i) : value(i) {}
int value;
};
int main() {
int i = 1;
int j = 2;
auto t1 = std::make_tuple(std::make_unique<Foo>(0), std::ref(i), std::cref(j), 4);
static_assert(std::is_same_v<decltype(t1),
std::tuple<std::unique_ptr<Foo>, int&, const int&, int>>);
auto t2 = replace_tuple_element<1>(std::move(t1), std::make_unique<Foo>(5));
static_assert(std::is_same_v<decltype(t2),
std::tuple<std::unique_ptr<Foo>, std::unique_ptr<Foo>, const int&, int>>);
auto t3 = replace_tuple_element<0>(std::move(t2), std::cref(i));
static_assert(std::is_same_v<decltype(t3),
std::tuple<const int&, std::unique_ptr<Foo>, const int&, int>>);
auto t4 = replace_tuple_element<2>(std::move(t3), i);
static_assert(std::is_same_v<decltype(t4),
std::tuple<const int&, std::unique_ptr<Foo>, int, int>>);
}
Full demo with run-time asserts
This should do it:
template<std::size_t N, class U, class T>
auto replace_tuple_element(T&& t, U&& u) {
return [&]<std::size_t... I>(std::index_sequence<I...>) {
return std::tuple<std::conditional_t<I == N, U, std::tuple_element_t<I, std::decay_t<T>>>...>{
[&]() -> decltype(auto) {
if constexpr (I == N) return std::forward<U>(u);
else return static_cast<std::tuple_element_t<I, std::decay_t<T>>>(std::get<I>(t));
}()...};
}(std::make_index_sequence<std::tuple_size_v<std::decay_t<T>>>{});
}
You can remove some of the casts, forwards etc. if you're only concerned with value semantics.
The only thing new here is lambda template parameters to infer the indexing argument.
If we want to both preserve all the types exactly as they are, and also do the same kind of reference unwrapping thing that the standard library typically does, then we need to make a small change to what the other implementations are here.
unwrap_ref_decay
will do a decay_t
on the type, and then turn reference_wrapper<T>
into T&
. And using Boost.Mp11 for a few things that just make everything nicer:
template <size_t N, typename OldTuple, typename NewType>
constexpr auto replace_tuple_element(OldTuple&& tuple, NewType&& elem)
{
using Old = std::remove_cvref_t<OldTuple>;
using R = mp_replace_at_c<Old, N, std::unwrap_ref_decay_t<NewType>>;
static constexpr auto Size = mp_size<Old>::value;
auto get_nth = [&](auto I) -> decltype(auto) {
if constexpr (I == N) return std::forward<NewType>(elem);
else return std::get<I>(std::forward<OldTuple>(tuple));
};
return [&]<size_t... Is>(std::index_sequence<Is...>) {
return R(get_nth(mp_size_t<Is>())...);
}(std::make_index_sequence<Size>());
}
This implementation means that given:
std::tuple<int const, int const> x(1, 2);
int i = 42;
auto y = replace_tuple_element<1>(x, std::ref(i));
y
is a tuple<int const, int&>
.
This is a good use case for a counterpart of tuple_cat
that, instead of concatenating tuples, gives you a slices of a tuple. Unfortunately, this doesn't exist in the standard library, so we'll have to write it ourselves:
template <std::size_t Begin, std::size_t End, typename Tuple>
constexpr auto tuple_slice(Tuple&& t)
{
return [&]<std::size_t... Ids> (std::index_sequence<Ids...>)
{
return std::tuple<std::tuple_element_t<Ids, std::remove_reference_t<Tuple>>...>
{std::get<Begin + Ids>(std::forward<Tuple>(t))...};
} (std::make_index_sequence<End - Begin>{});
}
Just like tuple_cat
, this preserve the exact same types of the original tuple.
With tuple_cat
and tuple_slice
, the implementation of replace_tuple_element
feels quite elegant:
template <std::size_t N, typename Tuple, typename T>
constexpr auto replace_tuple_element(Tuple&& tuple, T&& t)
{
constexpr auto Size = std::tuple_size_v<std::remove_reference_t<Tuple>>;
return std::tuple_cat(
tuple_slice<0, N>(std::forward<Tuple>(tuple)),
std::make_tuple(std::forward<T>(t)),
tuple_slice<N + 1, Size>(std::forward<Tuple>(tuple))
);
}
Using make_tuple
preserves the behavior of turning reference_wrapper<T>
into T&
. Demo
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