Заменить N-й элемент std :: tuple - PullRequest
6 голосов
/ 09 января 2020

Какой самый короткий / лучший способ заменить n-й элемент кортежа значением (которое может иметь или не иметь другой тип)? Решения, включая C ++ 20, в порядке. [РЕДАКТИРОВАТЬ: Я бы предпочел что-то, не требующее других библиотек, но мне все еще интересно, какие решения возможны, например, с Boost].

Т.е.:

#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));
}

Примечание: просто замена n -ый тип в списке типов, например, обсуждался здесь: Как заменить элемент кортежа во время компиляции? . Но я также хочу заменить это значение и надеюсь, что в c ++ 20 теперь есть более простые / более элегантные решения, чем когда-то, когда задавался этот вопрос.

Ответы [ 5 ]

5 голосов
/ 09 января 2020

Одно решение, которое я нашел для c ++ 20, таково:

#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));
}

Что мне нравится в решении, так это то, что оно представляет собой отдельную самодостаточную функцию. Интересно, есть ли что-то еще более короткое и / или более читаемое?

4 голосов
/ 09 января 2020

Возможное решение:

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>>{});
}

Обратите внимание, что это приводит к разложению всех типов элементов, удаляя ссылки и константы.

Эта поправка частично решает эту проблему:

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>>{});
}

Теперь replace_tuple_element также следует соглашению std::make_tuple, которое преобразует std::reference_wrapper аргументы в ссылки. Он сохраняет ссылочные типы, но сбрасывает константу верхнего уровня.

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>>);
}

Полная демонстрация с утверждениями времени выполнения

3 голосов
/ 10 января 2020

Если мы хотим сохранить оба типа в точности так, как они есть, а также сделать то же самое, что и стандартная библиотека, для развертывания ссылок, то нам нужно внести небольшое изменение в то, что здесь есть в других реализациях.

unwrap_ref_decay сделает тип decay_t, а затем превратит reference_wrapper<T> в T&. И использование Boost.Mp11 для нескольких вещей, которые только делают все лучше:

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>()); 
}

Эта реализация означает, что дано:

std::tuple<int const, int const> x(1, 2);
int i = 42;
auto y = replace_tuple_element<1>(x, std::ref(i));

y - это tuple<int const, int&>.

3 голосов
/ 09 января 2020

Это должно сделать это:

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>>>{});
}

Вы можете удалить некоторые броски, вперёд и т. Д. c. если вас интересует только семантика значений.

Единственное, что здесь нового - параметры лямбда-шаблона для вывода аргумента индексации.

2 голосов
/ 10 января 2020

Это хороший вариант использования для аналога tuple_cat, который вместо конкатенации кортежей дает вам кусочки кортежа. К сожалению, этого нет в стандартной библиотеке, поэтому нам придется написать это самим:

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>{});
}

Так же, как и tuple_cat, это сохраняет те же типы исходного кортежа.

С tuple_cat и tuple_slice реализация replace_tuple_element выглядит довольно элегантно:

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))
    );
}

Использование make_tuple сохраняет поведение превращения reference_wrapper<T> в T&. Демо

...