从元组中解包参数

9

我正在尝试弄清楚这个问题的工作原理:C++11:我可以从多个参数转换为元组,但我能否从元组转换为多个参数?

我不理解的黑魔法片段是这段代码:

f(std::get<N>(std::forward<Tuple>(t))...)

我不理解 f 中的表达式。

我理解这个表达式会以某种方式将 t 中的内容展开/扩展为参数列表。但有人能解释一下是如何实现的吗?当我查看 std::get 的定义时 (http://en.cppreference.com/w/cpp/utility/tuple/get),我不知道 N 是如何使用的...? 据我所知,N 是一个整数序列。

根据我的观察,我猜想以 E<X>... 形式的表达式,其中 X 是类型序列 X1X2、... Xn,该表达式将被扩展为 E<X1>, E<X2> ... E<Xn>。这是它的工作原理吗?

编辑:在这种情况下,N 不是类型序列,而是整数。但我猜测这种语言结构适用于类型和值。


2
是的,是的,是的,还有是的。它基本上扩展为 get<0>(t), get<1>(t), get<2>(t), ..., get<N>(t) - Xeo
6
@Daniel:这里使用std::forward是完全可以的,因为它只提取元组元素,因此只会移动它们。如果你知道自己在做什么,我不认为这是一个反模式,也不认为存在问题,就像你所说的那样。 - Xeo
2
@DanielFrey 我认为这不是未定义行为。[tuple.elem]/3 指定 get(tuple<Types...>&& t) 的效果等同于 return std::forward<typename tuple_element<I, tuple<Types...> >::type&&>(get<I>(t));。因此,元组并没有实际上被移动,只有包含的元素被转发(forward 本身是指定为一个 static_cast)。 - dyp
8
大家一起重复这个口号:“_std::move不移动,而std::forward<>不转发_” :) - sehe
1
@Yakk get 根据参数的 rvalueness 和元组中存储的类型,返回一个 lvalue 或 rvalue 引用。如果元组存储了一个 lvalue 引用,即使将元组作为 rvalue 引用传递,我们也不应该移动它。只有当存储的类型是 rvalue 引用时,std::get<N>(t) 才会返回一个 rvalue 引用,但在存储了 lvalue 引用或非引用类型的情况下,它将返回一个 lvalue 引用。 - dyp
显示剩余6条评论
2个回答

6

我认为@Xeo的评论已经很好地总结了这个问题。来自C++11标准的14.5.3:

打包展开由一个模式和一个省略号组成,它们的实例化在列表中生成零个或多个模式的实例。

在您的情况下,递归模板实例化结束并进入部分特化时,您有:

f(std::get<N>(std::forward<Tuple>(t))...);

...其中N是由四个int0123)组成的参数包。根据上面的标准,这里的模式

std::get<N>(std::forward<Tuple>(t))
...省略号的应用于上述模式会导致其在列表形式中展开成为四个实例,即:
f(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t));

2

扩展std::tuple<T...>的基本要素实际上在代码中被省略了:你需要获得第二个参数返回值:除了std::tuple<...>类型列表外,你还需要一个带有索引0, 1, ..., n的参数包。一旦你获得了这两个参数包,就可以同时扩展它们:

template <typename F, typename... T, int... N>
void call_impl(F&& fun, std::tuple<T...>&& t) {
    fun(std::get<N>(t)...);
}

真正的魔法在于当你只有一个std::tuple<T...>时,召唤第二个参数包。这需要一些模板编程技巧。下面是一种创建索引列表的方法:

template <int... Indices> struct indices;
template <> struct indices<-1> { typedef indices<> type; };
template <int... Indices>
struct indices<0, Indices...>
{
    typedef indices<0, Indices...> type;
};
template <int Index, int... Indices>
struct indices<Index, Indices...>
{
    typedef typename indices<Index - 1, Index, Indices...>::type type;
};

template <typename T>
typename indices<std::tuple_size<T>::value - 1>::type const*
make_indices()
{
    return 0;
}

如果您有一个函数模板,假设它叫做call(),它接受一个函数对象和一个带有函数参数的std::tuple<T...>。一种简单的方法是重写上述提到的call_impl()以处理推导索引:

template <typename F, typename Tuple, int... N>
void call_impl(F&& fun, Tuple&& t, indices<Indices...> const*)
{
    fun(std::get<N>(t)...);
}

template <typename F, typename Tuple>
void call(F&& fun, Tuple&& t)
{
    call_imle(std::forward<F>(fun), std::forward<Tuple>(t), make_indices<Tuple>());
}

这段代码真正需要扩展的是在调用函数时,正确使用各种std::tuple<...>元素和std::forward<...>()的方法。仅仅使用std::forward<Tuple>(t)不行的,因为它可能会移动整个std::tuple<...>,而非移动元素。我认为可以进行适当的逐个元素移动std::tuple<...>,但我还没有这样做过。

1
make_indices 中,为什么要使用 0 而不是 nullptr?(并且为什么要使用 const* 而不是按值传递,使用 return {};?) - dyp
@DyP:nullptr vs. 0:我只是还没有改用nullptr。索引列表也可以直接使用,但我认为这并不重要。 - Dietmar Kühl
我不明白它是如何移动整个元组的。标准规定它只会移动 rvalue 参数上的指定元素到 std::get,对吗? - Xeo
当然,包的范围是0,1,2,...,n-1而不是...,n。其次,为什么要返回一个指向索引的nullptr指针,而不是一个实际的(无状态的)索引实例? - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接