a.lasram
a.lasram

Reputation: 4411

perfect forwarding and order of function arguments evaluation

Is there a good way to forward arguments of a function f to a function g in a situation like

template<typename... T>
void g(T && ... x);

template<typename... T>
void f(T && ... x)
{
    g(x..., x...);
}

In the next code x could be moved twice

template<typename... T>
void f(T && ... x)
{
    g(std::forward<T>(x)..., std::forward<T>(x)...);
}

In the next code std::forward<T>(x)... could be evaluated before x...

template<typename... T>
void f(T && ... x)
{
    g(x..., std::forward<T>(x)...);
}

Upvotes: 2

Views: 350

Answers (2)

sehe
sehe

Reputation: 392833

You can in general force the order using

  • using separate statements (obviously)
  • expressions separated by the comma operator. (Beware of overloaded operator,)
  • The use of brace initialization works because the order of evaluation of the arguments in a brace initializer list is the order in which they appear1. The following has well-defined evaluation order:

    std::tuple<T..., T...> args { 
         std::forward<T>(x)..., 
         std::forward<T>(x)... }; // still not sane, but evaluation order defined
    

But it's still useless as g(...) might still move from the same reference twice. What you'd actually want for rvalue refs is not:

g(rvalue, std::move(rvalue));            // or
g(std::move(rvalue), rvalue);            // or even
g(std::move(rvalue), std::move(rvalue)); // [sic]

The only sane way would be:

g(lvalue=std::move(rvalue), lvalue); // BUT: fix the evaluation order

So how do we achieve precisely that but generically?

Enter Indices?!

Let's say you have variadic g as you described:

template<typename... T>
void g(T && ... x) 
{ 
}

Now, you can duplicate the arguments passed to f using

  1. the index trick:

    namespace detail // indices 
    {
        template<std::size_t... Is> struct seq{};
        template<std::size_t I, std::size_t... Is>
            struct gen_seq : gen_seq<I-1, I-1, Is...>{};
        template<std::size_t... Is>
            struct gen_seq<0, Is...>{ using type = seq<Is...>; };
    }
    
  2. and an invoker helper function:

    #include <tuple>
    
    template<typename Tuple, std::size_t... Is>
    void f_invoke_helper(Tuple const& tup, detail::seq<Is...>) 
    { 
        g(std::get<Is>(tup)..., std::get<Is>(tup)...);
    }
    

All that's required next is to tie it all together:

template<typename... T>
void f(T && ... x)
{
    f_invoke_helper(
            std::make_tuple(std::forward<T>(x)...), 
            typename detail::gen_seq<sizeof...(T)>::type());
}

Note that if you pass rvalue-refs, it will get moved once (into the tuple) and used twice (as a lvalue) in the invoker helper:

int main()
{
    std::string x = "Hello world";
    int i = 42;

    // no problem:
    f(i, -42, std::move(x));
}

Hope this helps!

PS. As it has been aptly pointed out, it's probably a lot easier to just say

template<typename... T>
void f(T&&... x) { g(x..., x...); }

I haven't thought of a way in which the tuple idiom doesn't result in the same, except for actually moving movable arguments into the tuple.


1The semantics of T{...} are described in 12.6.1
See also: how to avoid undefined execution order for the constructors when using std::make_tuple .

Upvotes: 2

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275230

std::forward doesn't move things - it creates a reference that says "it is ok to move from me". The actual moving occurs inside g, not in f where std::forward or std::move is called.

The problem of move is only one of the problems here. There is also the problem of passing the same object twice as a reference in two spots, which is generally considered quite rude!

We can fix that by creating temporary objects in f and pass those by reference, but that leads to a serious issue: references are often used to return values from a function, and we have the same variable being used twice -- we cannot return both results.

So the answer is "don't do that", because it is not in general safe. You have to know the semantics of both g and f to figure out what the correct thing to do is, and a simple forwarding type interface won't reflect the depth of knowledge required.

If you do have deep semantic understanding of what g and f are supposed to do, then the situation changes.

Upvotes: 6

Related Questions