9

So I'm trying to figure out how this works: C++11: I can go from multiple args to tuple, but can I go from tuple to multiple args?

The piece of black magic I do not understand is this code fragment:

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

it's the expression inside f that I don't understand.

I understand that the expression somehow unpacks/expands what's inside t into a list of arguments. But could someone care to explain how this is done? When I look at the definition of std::get (http://en.cppreference.com/w/cpp/utility/tuple/get), I don't see how N fits in...? As far as I can tell, N is a sequence of integers.

Based on what I can observe, I'm assuming that expressions in the form E<X>... where X is the sequence of types X1. X2, ... Xn, the expression will be expanded as E<X1>, E<X2> ... E<Xn>. Is this how it works?

Edit: In this case N is not a sequence of types, but integers. But I'm guessing this language construct applies to both types and values.

Community
  • 1
  • 1
Jörgen Sigvardsson
  • 4,839
  • 3
  • 28
  • 51
  • 2
    Yes, yes, yes, and yes. It basically expands to `get<0>(t), get<1>(t), get<2>(t), ..., get(t)` – Xeo Sep 24 '13 at 21:23
  • 1
    ***BIG FAT WARNING***: Never ever use `std::forward` or `std::move` within argument pack expansion - a value (here the tuple `t`) is only allowed to be moved **once**. It might work here because `std::get` does not actually move, but the above is in itself an **anti-pattern** one should be able to spot and then fix! – Daniel Frey Sep 24 '13 at 21:29
  • 6
    @Daniel: `std::forward` here is perfectly fine, since it extracts only the tuple-elements and thus only moves them. If you know what you're doing, I don't see the problem and I don't think it's an anti-pattern quite like you make it out to be. – Xeo Sep 24 '13 at 21:34
  • @Xeo Even if it seems to work, I think it's UB. I would accept `std::move(std::get(t))...`, so yes, there are exceptions which people with enough knowledge (like you and some of the regulars here) could use, but for most people it should be a red flag. – Daniel Frey Sep 24 '13 at 21:53
  • 2
    @DanielFrey I don't think it's UB. [tuple.elem]/3 specifies the effects of `get(tuple&& t)` as equivalent to `return std::forward >::type&&>(get(t));`. So the tuple isn't *actually* moved, only the contained element forwarded. (`forward` itself is specified as a `static_cast`) – dyp Sep 24 '13 at 21:59
  • 1
    @DyP OK, accepted. I was probably too focus on the pattern and it's still something I'd recommend users in general to avoid, although in this specific case, it is OK as the standard explicitly defines it as quoted. In real-world code, I'd still appreciate a comment on something like that as it is anything but obvious and might be something that a beginner might just copy somewhere else and then modify it a bit without noticing how dangerous something like this can be. I might be too cautious since we had a code like that in our companies code-base and it's a hell of thing to debug! – Daniel Frey Sep 24 '13 at 22:06
  • 8
    Everyone, repeat the mantra: "_`std::move` doesn't move and `std::forward<>` doesn't forward_" :) – sehe Sep 24 '13 at 22:21
  • 1
    Despite the fact it is quite well defined here, it is a code smell that you are `forward`ing in a context where actually `move`ing wouldn't be good. An approach to fix this is to write a `smart_forward(T&&)`, which evaluates to `std::move` iff `U` is not a reference. Then we'd get: `f(smart_forward(std::get(t))...)`, which does the conditional `move` on directly on data we actually want to move. (`smart_forward` is a poor name for this function... suggestions?) – Yakk - Adam Nevraumont Sep 25 '13 at 02:05
  • @Yakk What's the difference between your `smart_forward` and `std::forward`? – dyp Sep 25 '13 at 18:54
  • @DyP `std::forward(T??)` takes a `T` type parameter and returns possibly a `T&&`. `smart_forward(T??)` takes a `U` type parameter, and iff `U` is a non-reference type, returns an rvalue reference, otherwise it returns an lvalue reference. The idea is to enable conditional moving from a member or sub-component of `U` rather than conditionally moving all of `U`. Maybe `sub_forward` as a name? – Yakk - Adam Nevraumont Sep 25 '13 at 19:24
  • 1
    @Yakk `get` returns an lvalue or rvalue ref, depending on the rvalueness of the parameter **and** depending on the type stored in the tuple. If the tuple stores an lvalue ref, we shouldn't move even if the tuple has been passed as an rvalue ref. `std::get(t)` will only return an rvalue ref if the stored type is an rvalue ref, but it returns an lvalue ref in the case of a stored lvalue-reference or non-reference type. – dyp Sep 25 '13 at 22:20

2 Answers2

6

I think that @Xeo's comment summed it up well. From 14.5.3 of the C++11 standard:

A pack expansion consists of a pattern and an ellipsis, the instantiation of which produces zero or more instantiations of the pattern in a list.

In your case, by the time you finish with the recursive template instantiation and end up in the partial specialization, you have

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

...where N is parameter pack of four ints (0, 1, 2, and 3). From the standardese above, the pattern here is

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

The application of the ... ellipsis to the above pattern causes it to be expanded into four instantiations in list form, i.e.

f(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t));
Nate Kohl
  • 35,264
  • 10
  • 43
  • 55
2

The fundamental ingredient to expanding the std::tuple<T...> is actually omitted from the code: you need to obtain a a second parameter back: in addition to the list of types of the std::tuple<...> you need a parameter pack with indices 0, 1, ..., n. Once you have these two parameters packs, you can expand them in tandem:

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

The real magic lies in conjuring up the second parameter pack when you just have a std::tuple<T...>. It takes a bit of template programming. Here is an approach to create the list of indices:

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

So, if you have a function template, let's call it call() which takes a function object and a std::tuple<T...> with the arguments to the function. An easy approach is to rewrite the call_impl() mentioned above to deal with deducing the indices:

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

What this code doesn't really extend is the correct use of std::forward<...>() with the various std::tuple<...> elements when calling the function. Just using std::forward<Tuple>(t) does not work because it possibly moves the entire std::tuple<...> rather than moving the elements. I think something like a suitable element-wise move of a std::tuple<...> can be done but I haven't done it, yet.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • 1
    Is there a special reason for `0` instead of `nullptr` in `make_indices`? (And a reason for `const*` instead of by-value, using `return {};`?) – dyp Sep 24 '13 at 23:29
  • @DyP: `nullptr` vs. `0`: I just haven't changed to use `nullptr`. The index list could also be used directly but I don't think it matters. – Dietmar Kühl Sep 24 '13 at 23:37
  • I don't see how it could move the entire tuple. The standard says that it only moves the specified element on an rvalue argument to `std::get`, no? – Xeo Sep 24 '13 at 23:56
  • surely the pack is `0,1,2,...,n-1` not `...,n`. Second, why return a `nullptr` pointer-to-indexes instead of an actual (stateless) indexes instance? – Yakk - Adam Nevraumont Sep 25 '13 at 02:06