简体   繁体   English

替换 std::tuple 的第 N 个元素

[英]Replace N-th element of a std::tuple

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)?用值(可能有也可能没有不同类型)替换元组的第 n 个元素的最短/最佳方法是什么? Solutions including c++20 are fine.包括 c++20 在内的解决方案很好。 [EDIT: I would prefer something not requiring other libraries, but I'm still interested what solutions are possible with eg boost]. [编辑:我更喜欢不需要其他库的东西,但我仍然对使用例如 boost 可能的解决方案感兴趣]。

Ie: IE:

#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 eg be discussed here: How do I replace a tuple element at compile time?注意:仅替换类型列表中的第 n 个类型已经在这里讨论过: 如何在编译时替换元组元素? . . 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.但我也想替换这个值,并希望现在在 c++20 中有比问这个问题时更简单/更优雅的解决方案。

One solution I found for c++20 is this:我为 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));
}

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.现在replace_tuple_element也遵循std::make_tuple ,将std::reference_wrapper参数转换为引用。 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.这里唯一的新东西是用于推断索引参数的 lambda 模板参数。

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& . unwrap_ref_decay会对类型做一个decay_t ,然后把reference_wrapper<T>变成T& And using Boost.Mp11 for a few things that just make everything nicer:使用 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>()); 
}

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&> . y是一个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.这是tuple_cat一个很好的用例,它不是连接元组,而是给你一个元组的切片。 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.就像tuple_cat一样,它保留了与原始元组完全相同的类型。

With tuple_cat and tuple_slice , the implementation of replace_tuple_element feels quite elegant:使用tuple_cattuple_slicereplace_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))
    );
}

Using make_tuple preserves the behavior of turning reference_wrapper<T> into T& .使用make_tuple保留了将reference_wrapper<T>转换为T& Demo演示

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM