C++:在没有 copying/moving 的情况下使用引用和值解压元组
C++: Unpack a tuple with references and values without copying/moving too much
假设我有一个类型为 std::tuple<T1,T2,T3>
的元组 t
,其中每个 T
可能是 Obj
、Obj&
、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)));
}
但是当 TUP
是 std::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> a
、std::tuple<int&> b
和 std::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
1:std::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)));
}
假设我有一个类型为 std::tuple<T1,T2,T3>
的元组 t
,其中每个 T
可能是 Obj
、Obj&
、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)));
}
但是当 TUP
是 std::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> a
、std::tuple<int&> b
和 std::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
1:std::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)));
}