Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace N-th element of a std::tuple

Tags:

c++

c++20

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.

like image 635
MikeMB Avatar asked Jan 09 '20 19:01

MikeMB


People also ask

How to use tuples in C++?

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.

What is a tuple in Python?

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.

Is it possible to convert a tuple to a list?

But there is a workaround. You can convert the tuple into a list, change the list, and convert the list back into a tuple.

How to get an element from a tuple by Index?

One can get an element from std::tupleby index using std::get. Analogically, how to settuple's element by index? c++templatesindexingtuples


5 Answers

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.

like image 167
MikeMB Avatar answered Oct 23 '22 21:10

MikeMB


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

like image 32
Evg Avatar answered Oct 23 '22 19:10

Evg


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.

like image 23
ecatmur Avatar answered Oct 23 '22 20:10

ecatmur


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&>.

like image 3
Barry Avatar answered Oct 23 '22 20:10

Barry


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

like image 3
sebrockm Avatar answered Oct 23 '22 20:10

sebrockm