11

I have seen multiple instances of code where function parameter pack is declared using the && notation, as shown below, but I cannot see any advantage to using this notation.

template<typename... Args>
void Function(Args... args)
{
}

template<typename... Args>
void Function(Args&&... args)
{
}

My first thought was that the && form will be used exclusively for r-value objects, but this test proved that wrong:

struct Object
{
    // Added bodies so I see what is being called via a step-into
    Object() {}
    Object(const Object&) {}
    Object(Object&&) noexcept {}
    Object& operator=(const Object&) { return *this; }
    Object& operator=(Object&&) noexcept { return *this; }
};

Object GetObject() { Object o; return o; }

Object obj;

Function(GetObject());  
Function(GetObject());

Here, VS 2017 complains that both forms of the function are viable candidates for the call.

Can someone explain what the difference is between these two, and what advantages one may have over the other please?

Wad
  • 1,454
  • 1
  • 16
  • 33
  • 2
    when doing `template `, the `T&&` is not exactly your typical r-value reference. It's a forwarding reference. Lookup [`std::forward`](http://en.cppreference.com/w/cpp/utility/forward) – Fureeish Jan 18 '18 at 17:40
  • 2
    To achieve [perfect forwarding](https://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c/) – AMA Jan 18 '18 at 17:42

3 Answers3

7

They are forwarding references in the parameter pack form. As for template parameter deduction, they can match any arguments, but the template parameter will be deduced differently comparing to the ordinary template parameter.

The major advantage of forwarding reference is that the lvalue/rvalue information will be preserved if used with std::forward. Thus they are used to "forward" something.

For example,

void real_foo(A const &a);
void real_foo(A &&a);

template<class... Args>
void foo_proxy_ordinary(Args... args) { real_foo(args...); }

template<class... Args>
void foo_proxy_perfect(Args&&... args) { real_foo(std::forward<Args>(args)...); }

The ordinary version will always call real_foo(A const &) version, because inside foo_proxy, args are always lvalue.

However, the perfect version will select real_foo(A&&) if the arguments passed in are indeed rvalues.

Combining forwarding reference with parameter pack, one can write easily generic proxy functions without performance loss in terms of lvalue/rvalue.

llllllllll
  • 16,169
  • 4
  • 31
  • 54
  • OK, I tried this code and I see what you mean. However, I then coded `void foo_proxy_ordinary(Args... args) { real_foo(std::forward(args)...); }` and it **also** calls `real_foo(A &&a);` - so I get perfect forwarding even when I don't have `Args&&`! Can you please explain this? – Wad Jan 18 '18 at 20:23
  • my calling code is `class A{}; A a; foo_proxy_ordinary(std::move(a));` – Wad Jan 18 '18 at 20:28
  • 1
    If you use `std::forward` in `foo_proxy_ordinary`, then no matter r/lvalue you pass into the proxy, it is *always* the rvalue version of `real_foo` being called. Precisely, `A a; foo_proxy_ordinary(a);` will also invoke the rvalue `real_foo`. This will definitely destroy your program. – llllllllll Jan 18 '18 at 20:33
  • The implementation of `std::forward` relies on some special template parameter deduction rules, which will make template parameter `T` itself a lvalue reference when the passed argument is a lvalue. Thus ensure the correct behavior. If you are interested in, you just need to read the `std::forward` signature from cppreference. – llllllllll Jan 18 '18 at 20:37
  • Right, ok. I found the reference collapsing rules [here](http://thbecker.net/articles/rvalue_references/section_08.html), which agree with the post by user2079303 earlier. If I pass an `A&` to `foo_proxy_ordinary()`, we have `Args` as `A& &&`, which collapses to `A&`. So why is `real_foo(A&&)` being called? (Sorry) – Wad Jan 18 '18 at 20:46
  • 1
    If you pass an `A&` to `foo_proxy_ordinary`, the deduced template parameter `T` is `A`, not `A&`. Thus `std::forward()` return `T&&`, i.e. `A&&`. Maybe you are confused by the template deduction rules in the ordinary case. See http://en.cppreference.com/w/cpp/language/template_argument_deduction for details. – llllllllll Jan 18 '18 at 20:51
  • Aha! I looked at the code for `std::forward` and it basically does `return static_cast(param);`; hence I get a `A&&` thus no reference collapsing rules need to be applied, thus I always get the version taking a rvalue reference! I understand ! :) Thanks for your help! – Wad Jan 18 '18 at 20:58
5

T&& when used in the context of

template<typename T>
void f(T&& t);

is called a forwarding reference sometimes also called a universal reference.

Main advantage of a forwarding reference is that combined with std::forward it enables achieving a so-called perfect forwarding: function template passing its arguments to another function as they are (lvalue as lvalue, rvalue as rvalue).

Now it is possible to create higher-order functions that take other functions as arguments or return them, or superior function-wrappers (e.g., std::make_shared), and do other cool things.

Here is some material that explains it much better and in more detail than I possibly can:

AMA
  • 4,114
  • 18
  • 32
1

Can someone explain what the difference is between these two, and what advantages one may have over the other please?

The difference is same for parameter packs as it is for individual parameters. Args declares an "object parameter" (pass by value) and Args&& declares a reference parameter (pass by reference).

Passing by reference allows one to avoid copying the argument when that is unnecessary. It also allows modifying the referred argument if the reference is non-const, which includes the possibility of moving from that object.

Passing by value makes it clear to the caller that the passed object will neither be modified, nor be referred to as a result of calling the function.


My first thought was that the && form will be used exclusively for r-value objects

As your test demonstrates, that is indeed an incorrect assumption. When Args is a deduced type i.e. auto or a template argument, Args&& can indeed be either an l-value reference or an r-value reference. Which one it is depends on what Args is deduced to be. This demonstrates the reference collapsing rules concisely:

typedef int&  lref;
typedef int&& rref;
int n;
lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&  note this case in particular
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

Using such reference allows forwarding i.e. re-binding into a new lvalue reference (when possible) or moving from the object (when possible) or copying (when neither of the previous is possible).

Because of this, Args&& is called a forwarding reference (or a universal reference) when Args is a deduced type.

eerorika
  • 232,697
  • 12
  • 197
  • 326