📜 ⬆️ ⬇️

Perfect transfer and universal references in C ++

Recently, a link to Eli Bendersky ’s article “Perfect forwarding and universal references in C ++” was published on isocpp.org. In this small article there is a simple answer to a simple question - to solve which problems and how to use rvalue links.

One of the innovations in C ++ 11, which aims to increase the efficiency of programs, is the family of emplace methods in STL containers. For example, the std :: vector method has the emplace_back method (almost an analogue of the push_back method) and the emplace method (almost the analogue of the insert method).
Here is a small example showing the purpose of these new methods:
class MyKlass { public: MyKlass(int ii_, float ff_) {...} private: {...} }; some function { std::vector<MyKlass> v; v.push_back(MyKlass(2, 3.14f)); v.emplace_back(2, 3.14f); } 

If you follow the calls to the constructors and destructors of the class MyKlass, during the push_back call you can see the following:


As you can see, quite a lot of work is being done, the greater amount of which is not very necessary, since the object passed to the push_back method is obviously an rvalue reference and is destroyed immediately after the execution of this expression. Thus, there is no reason to create and destroy a temporary object. Why, in this case, not to create an object right inside the vector? This is exactly what the emplace_back method does. For the expression from the example v.emplace_back (2, 3.14f), only one constructor is executed that creates an object inside the vector. Without the use of temporary objects. The emplace_back itself calls the MyKlass constructor and passes the necessary arguments to it. This behavior was made possible thanks to two innovations from C ++ 11: templates with a variable number of arguments (variadic templates) and perfect transmission (perfect forwarding). In this article I want to explain how the perfect transmission works and how to use it.

The problem of perfect transmission


Suppose there is some func function that accepts parameters of types E1, E2, ..., En. You want to create a wrapper function that accepts the same set of parameters. In other words, to define a function that will transfer the received parameters to another function without creating temporary variables, that is, it will perform an ideal transfer.
In order to specify the task, consider the method of emplace_back, which was described above. vector :: emplace_back passes its parameters to the constructor T without knowing anything about what T is.
The next step is to look at several examples showing how this behavior can be achieved without the use of the C ++ 11 innovations. For simplicity, we will not take into account the need to use templates with a variable number of argument parameters; suppose that only two arguments are required.
The first option that comes to mind:
 template <typename T1, typename T2> void wrapper(T1 e1, T2 e2) { func(e1, e2); } 

But this obviously will not work as needed if func takes parameters by reference, since the wrapper takes parameters by value. In this case, if func changes the parameters received by reference, this will not affect the parameters passed to the wrapper (copies created inside the wrapper will be modified).
Well, then we can remake the wrapper so that it takes the parameters by reference. This will not be a hindrance if func will be taken not by reference, but by value, since the func inside the wrapper will make the necessary copies for itself.
 template <typename T1, typename T2> void wrapper(T1& e1, T2& e2) { func(e1, e2); } 

Here is another problem. Rvalue cannot be passed to a function as a reference. Thus, a completely trivial call will not compile:
 wrapper(42, 3.14f); // :    rvalue- wrapper(i, foo_returning_float()); //    

And immediately there is no, if the thought came to make these links constant - this also does not solve the problem. Because func may require non-constant links as parameters.
All that remains is a rough approach used in some libraries: overload the function for constant non-constant references:
 template <typename T1, typename T2> void wrapper(T1& e1, T2& e2) { func(e1, e2); } template <typename T1, typename T2> void wrapper(const T1& e1, T2& e2) { func(e1, e2); } template <typename T1, typename T2> void wrapper(T1& e1, const T2& e2) { func(e1, e2); } template <typename T1, typename T2> void wrapper(const T1& e1, const T2& e2) { func(e1, e2); } 

Exponential growth. You can imagine how much fun it will bring when you need to handle some reasonable number of parameters of real functions. To make matters worse, C ++ 11 adds rvalue links, which also need to be taken into account in the wrapper function, and this is definitely not an extensible solution.
')

Link compression and special type inference for rvalue links


In order to explain how the perfect transfer is implemented in C ++ 11, you must first understand two new rules that have been added to this programming language.
Let's start with a simple one — link collapsing. As you know, taking a link to a link in C ++ is not allowed, but this can sometimes happen when implementing templates:
 template <typename T> void baz(T t) { T& k = t; } 

What happens if you call this function like this:
 int ii = 4; baz<int&>(ii); 

When instantiating a template, T is set equal to int &. What is the type of the variable k inside the function? The compiler "sees" int & & - and since this is a forbidden construction, the compiler simply converts it into a regular link. In fact, prior to C ++ 11, this behavior was not standardized, but many compilers accepted and converted such code, as it is often found in metaprogramming. After rvalue links were added to C ++ 11, it became important to determine the behavior when combining different types of links (for example, what does int && &? Mean).
So the link compression rule appeared. This rule is very simple - a single ampersand (&) always wins. Thus - (& & &) is (&), as well as (&& and &), and (& & &). The only case in which the result of compression is (&&) is (&& and &&). This rule can be compared with the result of a logical OR, where & is 1, and && is 0.
Another C ++ addition that is directly related to the topic under consideration is the special type deduction rules for rvalue references in various cases [1]. Consider an example of a template function:
 template <class T> void func(T&& t) { } 

Don't let a double ampersand fool you - t here is not an rvalue link [2]. When appearing in a given situation (when a special type inference is needed), T && takes on special significance - when func is instantiated, T changes depending on the type transferred. If a lvalue of type U is passed, then T becomes U &. If U is an rvalue, then T becomes just U. Example:
 func(4); // 4  rvalue: T  int double d = 3.14; func(d); // d  lvalue; T  double& float f() {...} func(f()); // f()  rvalue; T  float int bar(int i) { func(i); // i  lvalue; T  int& } 

This rule may seem unusual and even strange. It is. But, nevertheless, this rule becomes quite obvious when it comes to the understanding that this rule helps to solve the problem of perfect transmission.

Implementing perfect transmission using std :: forward


Now let's go back to our wrapper template function described above. Here is how it should be implemented using C ++ 11:
 template <typename T1, typename T2> void wrapper(T1&& e1, T2&& e2) { func(forward<T1>(e1), forward<T2>(e2)); } 

And this is how forward [3] is implemented:
 template<class T> T&& forward(typename std::remove_reference<T>::type& t) noexcept { return static_cast<T&&>(t); } 

Consider the following challenge:
 int ii ...; float ff ...; wrapper(ii, ff); 

Consider the first argument (the second is similar): ii is an lvalue, so T1 becomes int & according to the special type inference rule. The call to func (forward <int &> (e1), ...) is obtained. Thus, the forward pattern is instantiated by the int & type and we get the following version of this function:
 int& && forward(int& t) noexcept { return static_cast<int& &&>(t); } 

Time to apply link compression rule:
 int& forward(int& t) noexcept { return static_cast<int&>(t); } 

In other words, the argument is passed by reference to func, as required for lvalue.
The following example:
 wrapper(42, 3.14f); 

Here the arguments are rvalue, so T1 becomes an int. We receive call func (forward (e1), ...). Thus, the template function forward is instantiated by type int and we get the following version of the function:
 int&& forward(int& t) noexcept { return static_cast<int&&>(t); } 

The argument received by reference is converted to an rvalue reference, which is required to receive from forward.
The template function forward can be considered as some kind of wrapper over static_cast <T &&> (t), when T can take the value U & or U &&, depending on the type of input argument (lvalue or rvalue). Now the wrapper is one template that handles any combination of argument types.
The template forward function is implemented in C ++ 11, in the header file “utility”, in the namespace std.

Another point to note is the use of std :: remove_reference. In fact, forward can be implemented without using this function. Link compression will do all the work, so using std :: remove_reference for this is redundant. However, this function allows you to infer T & t in a situation where this type cannot be inferred (according to the C ++ standard, 14.8.2.5), therefore it is necessary to explicitly specify the template parameters when calling std :: forward.

Universal links


In his speeches, blog posts and books, Scott Myers gives the name “universal reference” for rvalue links, which are in the context of type inference. Successful is the name or not, it is difficult to say. As for me, when I first read the relevant chapter of the new book “Effective C ++”, I felt confused. More or less, everything became clear later when I figured out the underlying mechanisms (link compression and special type inference rules).
The trap is that the phrase “universal links” [4] is of course shorter and more beautiful than “rvalue links in the context of type inference”. But if there is a desire to actually understand some code, it will not be possible to avoid a complete description.

Examples of using perfect transmission


Ideal transfer is quite useful because it makes programming at a higher level possible. Higher-order functions are functions that can take other functions as arguments or return them. Without a perfect transfer, the use of higher-order functions is rather cumbersome, since there is no convenient way to pass arguments to a function inside a wrapper function. By the term “function”, here, in addition to the functions themselves, I also mean classes, the constructors of which are also functions.
At the beginning of this article, I described the container method emplace_back. Another good example is the standard make_unique template function, which I described in a previous article :
 template<typename T, typename... Args> unique_ptr<T> make_unique(Args&&... args) { return unique_ptr<T>(new T(std::forward<Args>(args)...)); } 

I admit honestly that in that article I simply ignored the strange double ampersand and focused on a variable number of template arguments. But now it is completely easy to fully understand the code. It goes without saying that the ideal transfer and templates with a variable number of arguments are very often used together, because, in most cases, it is not known how many arguments the function or constructor takes, to which we pass these arguments.
As an example with a much more complicated use of the ideal transfer, you can see the implementation of std :: bind.

References to sources


Here are some sources that helped me a lot when preparing the material:
  1. The 4th edition of The C ++ Programming Language by Bjarne Stroustrup
  2. The new "Effective Modern C ++" by Scott Myers. In this book, “universal links” are widely discussed. In fact, more than one fifth of this book is devoted to this topic.
  3. Technical paper n1385 : "The forwarding problem: Arguments".
  4. Thomas Becker C ++ Rvalue references explained - well written and very useful article

Notes:
[1] Auto and decltype can also be used, here I describe only the case of using a pattern.
[2] I consider as unsuccessful the decision of the C ++ Standardization Committee on the choice of notation for rvalue links (overload &&). Scott Myers admitted in his speech (and commented a bit on his blog) that after 3 years this material is still not easy to learn. And Bjorn Straustrup in The 4th edition of “The C ++ Programming Language”, when describing std :: forward, forgot the explicit indication of the template argument. It can be concluded that this is indeed a rather difficult area.
[3] This is a simplified version of std :: forward from STL C ++ 11. There is still an additional version that is explicitly overloaded for the rvalue arguments. I still try to figure out why it is needed. Let me know if there is any idea.
[4] Forwarding references are another designation that I have met.

From the translator: on CppCon2014, many (including Meyers, Stroustrup, Saffer) decided to use the term forwarding references instead of universal references .

A couple of articles on Habré on this topic:
Brief introduction to rvalue links
"Universal" links in C ++ 11 or T && do not always mean "Rvalue Reference"

Source: https://habr.com/ru/post/242639/


All Articles