How it works...

In this recipe, we will learn how to use perfect forwarding to ensure that, when we pass parameters in our templates (that is, forward our parameters), we do so in a way that doesn't erase r-valueness. To better understand the issue, let's look at the following example:

#include <iostream>

struct the_answer
{ };

void foo2(const the_answer &is)
{
std::cout << "l-value ";
}

void foo2(the_answer &&is)
{
std::cout << "r-value ";
}

template<typename T>
void foo1(T &&t)
{
foo2(t);
}

int main(void)
{
the_answer is;
foo1(is);
foo1(the_answer());

return 0;
}

The output is as follows:

In the preceding example, we have two different versions of a foo() function: one that takes an l-value reference and one that takes an r-value reference. We then call foo() from a template function. This template function takes a forwarding reference (also called a universal reference), which is an r-value reference paired with either auto or a template function. Finally, from our main function, we call our template to see which foo() function is called. The first time we call our template, we pass in an l-value. Since we are given an l-value, the universal reference becomes an l-value, and the l-value version of our foo() function is called. The problem is, the second time we call our template function, we give it an r-value, but it calls the l-value version of our foo() function, even though it was given an r-value.

The common mistake here is that even though the template function takes a universal reference and we have a version of the foo() function that also takes an r-value, we assume this foo() function would be called. Scott Meyers does a great job explaining this in many of his lectures on universal references. The problem is that the moment you use a universal reference, it becomes an l-value. The very act of passing the names parameter, which means it must be an l-value. It forces the compiler to convert to an l-value because it sees you using it, even though all you are doing is passing the parameter. It should be noted that our example doesn't compile with optimizations as the compiler is free to optimize the l-value out if it can safely determine that the variable is not used.

To prevent this issue, we need to tell the compiler that we wish to forward the parameter. Normally, we would use std::move() for this. The problem is, if we were originally given an l-value, we cannot use std::move() as that would convert an l-value into an r-value. This is why the standard library has std::forward(), which is implemented using the following:

static_cast<T&&>(t)

All std::forward() does is cast the parameter back to its original reference type. This tells the compiler explicitly to treat the parameter as an r-value if it was originally an r-value, as in the following example:

#include <iostream>

struct the_answer
{ };

void foo2(const the_answer &is)
{
std::cout << "l-value ";
}

void foo2(the_answer &&is)
{
std::cout << "r-value ";
}

template<typename T>
void foo1(T &&t)
{
foo2(std::forward<T>(t));
}

int main(void)
{
the_answer is;
foo1(is);
foo1(the_answer());

return 0;
}


The output is as follows:

The preceding example is identical to the first example with the only difference being that we pass the parameter in our template function using std::forward(). This time, when we call our template function with an r-value, it calls the r-value version of our foo() function. This is called perfect forwarding. It ensures that we maintain CV properties and l-/r-value properties when passing parameters. It should be noted that perfect forwarding only works when using template functions or auto. What this means is that perfect forwarding is usually only useful when writing wrappers. A good example of a standard library wrapper is std::make_unique().

One issue with a wrapper such as std::make_unique() is that you might not know how many parameters need to be passed. That is, you might end up needing variadic template arguments in your wrapper. Perfect forwarding supports this by doing the following:

#include <iostream>

struct the_answer
{ };

void foo2(const the_answer &is, int i)
{
std::cout << "l-value: " << i << ' ';
}

void foo2(the_answer &&is, int i)
{
std::cout << "r-value: " << i << ' ';
}

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

int main(void)
{
the_answer is;

foo1(is, 42);
foo1(the_answer(), 42);

return 0;
}

The output is as follows:

The preceding example works because the variadic template arguments being passed to our foo() function are replaced by a comma-separated list of perfect forwards.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset