When to Use Std::Forward to Forward Arguments

When to use std::forward to forward arguments?

Use it like your first example:

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

template <typename ...Args> void f(Args && ...args)
{
g(std::forward<Args>(args)...);
}

That's because of the reference collapsing rules: If T = U&, then T&& = U&, but if T = U&&, then T&& = U&&, so you always end up with the correct type inside the function body. Finally, you need forward to turn the lvalue-turned x (because it has a name now!) back into an rvalue reference if it was one initially.

You should not forward something more than once however, because that usually does not make sense: Forwarding means that you're potentially moving the argument all the way through to the final caller, and once it's moved it's gone, so you cannot then use it again (in the way you probably meant to).

Why should I use std::forward?

There are a number of good posts on what std::forward does and how it works (such as here and here).

In a nutshell, it preserves the value category of its argument. Perfect forwarding is there to ensure that the argument provided to a function is forwarded to another function (or used within the function) with the same value category (basically r-value vs. l-value) as originally provided. It is typically used with template functions where reference collapsing may have taken place (involving universal/forwarding references).

Consider the code sample below. Removing the std::forward would print out requires lvalue and adding the std::forward prints out requires rvalue. The func is overloaded based on whether it is an rvalue or an lvalue. Calling it without the std::forward calls the incorrect overload. The std::forward is required in this case as pass is called with an rvalue.

#include <utility>
#include <iostream>
class Test {
public:
Test() {
std::cout << "ctor" << std::endl;
}
Test(const Test&) {
std::cout << "copy ctor" << std::endl;
}
Test(Test&&) {
std::cout << "move ctor" << std::endl;
}
};

void func(Test const&)
{
std::cout << "requires lvalue" << std::endl;
}

void func(Test&&)
{
std::cout << "requires rvalue" << std::endl;
}

template<typename Arg>
void pass(Arg&& arg) {
// use arg here
func(std::forward<Arg>(arg));
return;
}

template<typename Arg, typename ...Args>
void pass(Arg&& arg, Args&&... args)
{
// use arg here
return pass(std::forward<Args>(args)...);
}

int main(int, char**)
{
pass(std::move<Test>(Test()));
return 0;
}

Consequences of and alternatives to use std::forward on non-forwarding-reference type template parameter

If a template argument is deduced other than from a forwarding reference, it is never deduced as a reference: then std::forward<T> is just the overload set

T&& forward(T&);
T&& forward(T&&);

which behaves exactly like std::move. If the function parameter was declared as T&, this is misleading: the argument will be moved from, so the function should probably accept an rvalue (or forwarding) reference instead.

If the function parameter is just T, std::forward is harmless since the parameters are your own objects, but you might as well just use std::move.

Your case, however, is a bit different: this interface requires explicit template arguments, which can be references at the discretion of the client. If it’s not a reference, it again behaves like std::move. If it is, then std::forward reproduces the same kind of reference, which seems to be the correct behavior.

For completeness, consider the case of explicitly specifying the template argument for a function that accepts T&: regardless of the reference status of the template argument, the function will accept an lvalue reference, and will move from it(!) if the template argument is itself not a reference or an rvalue reference. The same two cases will result in a move when forwarding a non-forwarding T&& parameter, but then the (function) argument must be an rvalue.

In conclusion, using std::forward<T> when the parameter is T& is wrong whether or not T is deduced, but is correct for T or T&& regardless. The quoted guideline is too strict in that case: the deduced-T case should just be std::move, but your usage is useful and correct.

Do I have to use std::forward when passing arguments pack to another variadic template function

You need to use forward. forward looks like this:

template <class T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept
{
return static_cast<T&&>(t);
}

(There is another overload of forward, but it is irrelevant here.)

For each element Arg of the template parameter pack Args, Arg can be either: (T is a non-reference type)

  • T& if the argument is a non-const lvalue, so that the parameter is of type T&; or

  • const T& if the argument is a const lvalue, so that the parameter is of type const T&; or

  • T if the argument is an rvalue, so that the parameter is of type T&&.

In the first case, forward<Arg>(arg) instantiates to:

constexpr T& forward(T& t) noexcept
{
return static_cast<T&>(t);
}

resulting in a non-const lvalue to be passed.

In the second case, forward<Arg>(arg) instantiates to:

constexpr const T& forward(const T& t) noexcept
{
return static_cast<const T&>(t);
}

resulting in a const lvalue to be passed.

In the last case, forward<Arg>(arg) instantiates to:

constexpr T&& forward(T&& t) noexcept
{
return static_cast<T&&>(t);
}

resulting in an rvalue to be passed.

In all three cases, the value category is preserved, and the value is forwarded with modification to bar.

If you don't use forward, you will be unconditionally passing an lvalue, which is not desired.

Do I need to perfect forward arguments in these cases where the arguments are used directly in the function?

An expression that is a name of a variable is always an lvalue. For example, in

1  template <typename T>
2 void push_back(T&& copy) {
3 *_end = copy;
4 }

copy in line 3 has the lvalue value category no matter what type is deduced for T. Depending on how operator= in line 3 is defined/overloaded, this might result in selecting a wrong overload or in wrong type deduction.

Consider the following simple example (that follows the Ted Lyngmo's example from the comments section):

struct A {
void operator=(const A&); // (1)
void operator=(A&&); // (2)
};

struct C {
template<class T>
void push_back(T&& copy) {
a = copy;
}
A a;
};

C{}.push_back(A{});

Which A's assignment operator will be invoked here? One might expect (2) because A{} is a prvalue, but the correct answer is (1), because copy is an lvalue, and lvalues can't bind to rvalue references.

If we change the assignment to

a = std::forward<T>(copy);

then the expression std::forward<T>(copy) will have the xvalue value category and the (2) assignment operator will be called, as expected.

Placement new follows the same reasoning.



Related Topics



Leave a reply



Submit