C++:在没有 copying/moving 的情况下使用引用和值解压元组

C++: Unpack a tuple with references and values without copying/moving too much

假设我有一个类型为 std::tuple<T1,T2,T3> 的元组 t,其中每个 T 可能是 ObjObj&Obj&&,const Obj&。我想编写一个函数,将元组的值解压缩到一个函数 f 中,该函数接受衰减到 Obj 的三种类型。我想尽可能避免无用的复制,我该怎么做?

我目前的实现是

static R call(C2&& c, TUP&& x) {
using TUP0 = typename std::remove_reference<TUP>::type;
return c(std::forward<typename std::tuple_element<0, TUP0>::type>(std::get<0>(x)),
         std::forward<typename std::tuple_element<1, TUP0>::type>(std::get<1>(x)),
         std::forward<typename std::tuple_element<2, TUP0>::type>(std::get<2>(x)));
}

但是当 TUPstd::tuple<Obj,Obj,Obj> 时,这个实现似乎 move 事情,即使它应该只在 TUP 包含 Obj&&.

时移动

在 C++17 中,这称为 std::apply():

static R call(C2&& c, TUP&& x) {
    return std::apply(std::forward<C2>(c), std::forward<TUP>(x));
}

这可以通过使用索引序列技巧在 C++11 中实现。 std::make_index_sequence 仅在 C++14 中添加到标准库中,但它本身也可在 C++11 中实现,我不会在此处包含该实现:

namespace detail {
    template <class F, class Tuple, size_t... Is>
    auto apply_impl(F&& f, Tuple&& t, index_sequence<Is...>) 
        -> decltype(std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))...))
    {
        return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))...);
    }
}

template <class F, class Tuple>
auto apply(F&& f, Tuple&& t)
    -> decltype(details::apply_impl(std::forward<F>(f), std::forward<Tuple>(t), make_index_sequence<std::tuple_size<typename std::decay<Tuple>::type>::value>{}))
{
    return details::apply_impl(std::forward<F>(f), std::forward<Tuple>(t),
        make_index_sequence<std::tuple_size<typename std::decay<Tuple>::type>::value>{});
}

似乎对 std::get() 的实际作用有些混淆。请注意,这取决于元组的引用限定。以下是相关的重载:

template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&
    get( tuple<Types...>& t );
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
    get( tuple<Types...>&& t );
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >const&
    get( const tuple<Types...>& t );
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >const&&
    get( const tuple<Types...>&& t );

返回类型匹配输入元组的 const 和 ref 限定。给定 std::tuple<int> astd::tuple<int&> bstd::tuple<int&&> c:

std::get<0>(a);            // int&
std::get<0>(std::move(a)); // int&&

std::get<0>(b);            // int&
std::get<0>(std::move(b)); // int&, because reference collapsing

std::get<0>(c);            // int&, because reference collapsing
std::get<0>(std::move(c)); // int&&

std::get<I>(std::forward<TUP>(x)) 为您提供正确、安全的引用类型,无论元组成员的类型如何。 std::get<0>(c) 给你一个左值引用——这是正确的行为。如果你想要一个右值引用,你需要一个右值。通常。

问题是 std::forward 会将 non-reference 类型转换为右值引用。您需要一个将 non-reference 类型转换为左值引用但在类型已经是引用时保留引用类别的函数:

template <typename T>
constexpr decltype(auto) stable_forward(std::remove_reference_t<T>& arg) {
    return static_cast<std::conditional_t<std::is_reference<T>::value, T, T&>>(arg);
}
template <typename T>
constexpr decltype(auto) stable_forward(std::remove_reference_t<T>&& arg) {
    return static_cast<std::conditional_t<std::is_reference<T>::value, T, T&&>>(arg);
}

static R call(C2&& c, TUP&& x) {
    using TUP0 = std::remove_reference_t<TUP>;
    return c(stable_forward<std::tuple_element_t<0, TUP0>>(std::get<0>(std::forward<TUP>(x))),
             stable_forward<std::tuple_element_t<1, TUP0>>(std::get<1>(std::forward<TUP>(x))),
             stable_forward<std::tuple_element_t<2, TUP0>>(std::get<2>(std::forward<TUP>(x))));
}

为 C++14 道歉,向 C++11 的转换留作 reader 的练习。

我已经按要求回答了问题。我不能说这是多么明智。如果您可以调整代码以将元组作为右值提供,Barry 的回答可能会更好地为您服务。

如果我答对了,期望的行为是:仅当元素类型为 "rvalue reference" 或元素类型为 "value in an rvalue tuple" 时才传递右值引用,否则传递左值引用.

1。如果是左值 T = std::tuple<Obj, Obj&, Obj&&>& 对象 tuple:

  • 不要移动第一个元素 (Obj) 1
  • 不要移动第二个元素(Obj&)
  • 移动第三个元素(Obj&&)2

1std::forward<std::tuple_element_t<I, T>>(std::get<I>(tuple)...) 不会做那个工作,因为结果将是 Obj&&std::get<I>(std::move(tuple))... 也不会给出所需的行为,因为结果将再次是 Obj&&.

2:此处 std::get<I>(std::forward<T>(tuple))... 不会给出所需的值类型,因为结果将为 Obj&

2。如果是右值 T = std::tuple<Obj, Obj&, Obj&&>&& 对象 tuple:

  • 移动第一个元素 (Obj)
  • 不要移动第二个元素(Obj&)
  • 移动第三个元素(Obj&&)

std::get<I>(std::forward<T>(tuple))... 将对右值起作用。


因此我认为你想要的是:

tuple<Obj>& -> Obj&
tuple<Obj&>& -> Obj&
tuple<Obj&&>& -> Obj&&

tuple<Obj>&& -> Obj&&
tuple<Obj&>&& -> Obj&
tuple<Obj&&>&& -> Obj&&

虽然@Oktalist 的答案会给你想要的行为,但还有另一种方法可以做到:

namespace detail
{
    template<std::size_t I, class ... Ts>
    decltype(auto) get(std::tuple<Ts...>& x)
    {
        using T = std::tuple_element_t<I, std::tuple<Ts...>>;
        return static_cast<std::conditional_t<
            std::is_reference<T>::value, T, T&>>(std::get<I>(x));
    }
    template<std::size_t I, class ... Ts>
    decltype(auto) get(std::tuple<Ts...>&& x)
    {
        return std::get<I>(std::move(x));
    }
}

template<class C2, class TUP>
decltype(auto) call(C2&& c, TUP&& x) {
    return std::forward<C2>(c)(
        detail::get<0>(std::forward<TUP>(x)),
        detail::get<1>(std::forward<TUP>(x)),
        detail::get<2>(std::forward<TUP>(x)));
}