Perfect Forwarding

Contents[Show]

Today, we solve " ... a herefore unsolved problem in C++" (Bjarne Stroustrup). To make the long story short, I will write about perfect forwarding.

 

But, what is perfect forwarding?

If a function templates forward its arguments without changing their lvalue or rvalue characteristics, we call it perfect forwarding.

Great. But what are lvalues and rvalues? Now, I have to make a little detour.

Lvalues and rvalues

I will not talk about the details about lvalues and rvalues and introduce therefore glvalues, xvalues ,and prvalues. That's not necessary. In case, you are curious, read the post from Anthony Williams: Core C++ - lvalues and rvalues. I will provide in my post a sustainable intuition.

Rvalues are

  • temporary objects.
  • objects without names.
  • objects which have no address.

If one of the characteristics holds for an object, it will be an rvalue. In reverse that means that lvalues have a name and an address. A few examples for rvalues:

int five= 5;
std::string a= std::string("Rvalue");
std::string b= std::string("R") +  std::string("value");
std::string c= a + b;
std::string d= std::move(b);

Rvalues are on the right side of an assignment. The value 5 and the constructor call are std::string("Rvalue") rvalues because you can either determine the address of the value 5 nor has the created string object a name. The same holds for the addition of the rvalues in the expression std::string("R") + std::string("value").

The addition of the two strings a + b is interesting. Both strings are lvalues, but the addition creates a temporary object. A special use case is std::move(b). The new C++11 function convert the lvalue b into a rvalue reference.

Rvalues are on the right side of an assignment; lvalues can be a the left side of an assignment. But that is not always true:

const int five= 5;
five= 6;

 

Although, the variable five is a lvalue. But five is constant and you can not use on the left side of an assignment.

But now to the challenge of this post: Perfect forwarding. To get an intuition for the unsolved problem, I will create a few perfect factory methods.

A perfect factory method

At first, a short disclaimer. The expression a perfect factory method is no formal term.

A perfect factory method is for me a totally generic factory method. In particular that means that the function should have the following characteristics:

  • Can take an arbitrary number of arguments
  • Can accept lvalues and rvalues as argument
  • Forwards it arguments identical to the underlying constructor

I want to say it less formal. A perfect factory method should be able to create each arbitrary object.

Lets start with the first iteration.

First iteration

For efficiency reasons the function template should take its arguments by reference. To say it exactly. As a non-constant lvalue reference. Here is the function template create in my first iteration. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// perfectForwarding1.cpp

#include <iostream>

template <typename T,typename Arg>
T create(Arg& a){
  return T(a);
}


int main(){
    
  std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::cout << std::endl;

}

 

If I compile the program, I will get a compiler error. The reason is that the rvalue (line 21) can not be bound to a non-constant lvalue reference.

perfectForwarding1

Now, I have to ways to solve the issue.

  1. Change the non-constant lvalue reference (line 6) in a constant lvalue reference. You can bind a rvalue to a constant lvalue reference. But that is not perfect, because the function argument is constant and I can therefore not change it.
  2. Overload the function template for a constant lvalue reference and a non-const lvalue reference. That is easy. That is the right way to go.

Second iteration

Here is the factory method create overloaded for a constant lvalue reference and a non-constant lvalue reference.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// perfectForwarding2.cpp

#include <iostream>

template <typename T,typename Arg>
T create(Arg& a){
  return T(a);
}

template <typename T,typename Arg>
T create(const Arg& a){
  return T(a);
}

int main(){
    
  std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::cout << std::endl;

}

 

The program produces the expected result.

perfectForwarding2

That was easy. Too easy. The solution has two conceptional issues.

  1. To support n different arguments, I have to overload  2^n +1 variations of the function template create. 2^n +1 because the function create without an argument is part of the perfect factory method.
  2. The function argument mutates in the function body of create to a lvalue, because it has a name. Does this matter? Of course, yes. a is not movable any more. Therefore, I have to perform an expensive copy instead of a cheap move. But what is even worse. If the constructor of T (line 12) needs a rvalue, it will not work any more.

 

Now, I have the solution in the shape of the C++ function std::forward.

Third iteration

With std::forward, the solution looks promising.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// perfectForwarding3.cpp

#include <iostream>

template <typename T,typename Arg>
T create(Arg&& a){
  return T(std::forward<Arg>(a));
}

int main(){
    
  std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::cout << std::endl;

}

 

Before I present the recipe from cppreference.com to get perfect forwarding, I will introduce the name universal reference.

The name universal reference is coined by Scott Meyers.

The universal reference (Arg&& a) in line 7 is a powerful reference that can bind lvalues or rvalues. You have it to your disposal if you declare a variable Arg&& a for a derived type A.

To achieve perfect forwarding you have to combine a universal reference with std::forward. std::forward<Arg>(a) returns the underlying type of a, because a is a universal reference. Therefore, a rvalue remains a rvalue.

Now to the pattern

template<class T>
void wrapper(T&& a){
    func(std::forward<T>(a)); 
}
 

I used the colour red to emphasis the key parts of the pattern. I used exactly this pattern in the function template create. Only the name of the type changed from T to Arg.

Is the function template create perfect? Sorry to say, but now. create needs exactly one argument which is perfectly forwarded to the constructor of the object (line 7). The last step in now to make a variadic template out of the function template.

Forth iteration - the perfect factory method

Variadic Templates are templates that can get an arbitrary number of arguments. That is exactly the missing feature of the perfect factory method.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// perfectForwarding4.cpp

#include <iostream>
#include <string>
#include <utility>

template <typename T, typename ... Args>
T create(Args&& ... args){
  return T(std::forward<Args>(args)...);
}

struct MyStruct{
  MyStruct(int i,double d,std::string s){}
};

int main(){
    
    std::cout << std::endl;

  // Lvalues
  int five=5;
  int myFive= create<int>(five);
  std::cout << "myFive: "  << myFive << std::endl;

  std::string str{"Lvalue"};
  std::string str2= create<std::string>(str);
  std::cout << "str2: " << str2 << std::endl;

  // Rvalues
  int myFive2= create<int>(5);
  std::cout << "myFive2: " << myFive2 << std::endl;

  std::string str3= create<std::string>(std::string("Rvalue"));
  std::cout << "str3: " << str3 << std::endl;

  std::string str4= create<std::string>(std::move(str3));
  std::cout << "str4: " << str4 << std::endl;
  
  // Arbitrary number of arguments
  double doub= create<double>();
  std::cout << "doub: " << doub << std::endl;
  
  MyStruct myStr= create<MyStruct>(2011,3.14,str4);


  std::cout << std::endl;

}

 

The three dots in line 7 -9 are the so called parameter pack. If the three dots (also called ellipse) are left of Args, the parameter pack will be packed; if right, the parameter pack will be unpacked. In particular, the three dots in line 9 std std::forward<Args>(args)... causes that each constructor call performs perfect forwarding. The result is impressive. Now, I can invoke the perfect factory method without (line 40) or with three arguments (line 43).

 perfectForwarding4

What's next?

 RAII, short for Resource Acquisition Is Initialization is a very import idiom in C++. Why? Read in the next post. 

 

 

 

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Get your e-book. Support my blog.

 

 

Add comment


Support my blog by buying my E-book

Latest comments

Modernes C++

Subscribe to the newsletter

Including two chapters of my e-book
Introduction and Multithreading

Blog archive

Source Code

Visitors

Today 2121

All 228135

Currently are 153 guests and no members online