Sunday 19 April 2015

Moving forward....

In the previous post, I tried my best explaining about 'move' semantics in C++11. In this post I am planning to cover up remaining things, namely:
1) Forwarding References
2) Reference Collapsing Rules
3) Forwarding Constructors
4) Type Limiting Forwarding Constructors
And as many examples as possible ! Also, all examples were compiled with g++ 4.8.2 and run on Ubuntu 14.02.

Since this post is a follow up on my previous post (the subject line does not indicate that), I would recommend to read through that before jumping here.

Link to previous post : Who moved my object ? No, not std::move!

Let's get started!!

Forwarding References

Now, since we are pretty much comfortable with R-Value references, lets go through some very simple examples and guess (it wouldn't be a guess if you know :) ) what would happen.

class Elf {
};

void take_elf(Elf&& elf) {  //..... (1)
    // do elf stuffs
}

template <typename T>
void templated_elf(T&& param) { // .. (6)
    // do elf stuffs 
}

int main() {
    Elf&& legolas = Elf();    // .... (2)  
    
    Elf&& dobby = legolas;    // .... (3)
    
    Elf rl_dobby = legolas;   // .... (4)

    take_elf(rl_dobby);     // .... (5)

    templated_elf(rl_dobby);// .... (7) 

    return 0;
}

Lets go through the code as per the numbering and find out issues as we go by them. Everything is being done with/on class Elf.

1. It is a function taking an r-value reference of type class Elf. Looks good.

2. Variable 'legolas' is an r-value reference and we are assigning it with an unnamed object or a temporary object of type class Elf. This is what the purpose of r-value reference is, i.e. binding with temporaries. So, all looks fine here.

[Important]
3. Here too, variable 'dobby' is an r-value reference, but in this case we are assigning it with 'legolas' instead of a temporary Elf object. The difference is that, a temporary variable is not an r-value reference . So, once a temporary is bound to an r-value reference for eg: 'legolas', it behaves just like an l-value from there on. That means, 'legolas' is an r-value reference at its point of definition at (2), but from there on, it behaves just like an l-value.
Thus, this statement would result in an compilation error (type mismatch), since we are trying to assign an l-value ('legolas') to an r-value reference which is 'dobby'.

4. If (3) is clear, this statement should appear as a regular assignment statement and there is nothing more to that. We are assigning 'legolas' to 'rl_dobby'. Since both are l-values here, there is no compilation error.

5. This is similar to (3). We are trying to call function 'take_elf' which expects an r-value reference as parameter with an l-value 'rl_dobby'. Same reasons mentioned for (3) holds true here as well.
So, this will result in compilation error (type mismatch). You would see the same error, if the call parameter is replaced to 'legolas'.

6. Welcome to 'Forwarding References'. You might now ask, what is the big difference ? It looks exactly like 'take_elf' except the fact that it is templated. Lets answer this in the next section.


Welcome to Forwarding References


So, your question was "what's the big difference? It's just templated", wasn't it ? Well, what makes it special is the template itself.

Had it been a regular template function, i.e. 'T' or 'T&' or 'T*' instead of 'T&&', it would have behaved just like its explicit type counterpart (mostly), except for the fact that the template version accepts all data types (the signature part).

But a template r-value reference or a forwarding reference (T&&) does not exhibit the same behavior as that of its explicit type counterpart for eg: 'take_elf'. The difference is that 'T&&' behaves like an r-value reference i.e binds with rvalues/ temporaries but they can also behave like an l-value !
This duality allows them to bind with both r-values and l-values.

Thus, statement (7) in the code is valid and does not result in compilation error like it did for (3). It is because of the dual nature of forwarding reference (T&&).

Besides this, they can also bind to const, volatile and both const-volatile objects making them 'greedy functions'. It is so greedy that we have a separate section on it.

We have already seen one such example of forwarding reference in my previous post, where a vector was being passed to a function 'do_work_on_thread' which used a forwarding reference. (Well, that example was slightly incorrect. There is an update beneath that section).

How to write a forwarding reference ?
Quoting from Scott Meyer's 'Effective Modern C++':
"""
For a reference to be universal, type deduction is necessary, but it's not sufficient. The form of the reference declaration must also be correct, and that form is quite constrained. It must be precisely "T&&" 
""" 
NOTE: Here, universal reference means forwarding reference. Initially, it was Scott Meyers who identified the need to have a different terminology for such references and he named it as 'universal' reference. Later, the standard committie standardized it as 'forwarding reference'.

Based upon the above, something like below is not a forwarding reference:

template <typename T>
void func(std::vector<T>&& arg) {...}

But this is a forwarding reference:
auto&& var = Test();


Reference Collapsing Rules

There are basically four rules associated with reference collapsing and is only applicable in case of forwarding references. These are a set of deduction rules which must be remembered while working with forwarding references.










The easiest way to remember is that, only when there are four '&', it collapses into an r-value reference, for all other cases it collapses into an l-value reference. :)

Below example depicts the first two rules:

#include <iostream>

template <typename T>
void func(T& val) {
    val += 10; // For the sake of simplicity,
               // hoping its always int :D
}

int main() {
    int a = 10; 
    int& ans = a;
    func(a);
    std::cout << a << " " << ans << std::endl; // prints 20 20

    int&& ans2 = std::move(a);
    func(a);
    std::cout << ans2 << std::endl; // prints 30

    return 0;
}


Below example depicts the remaining 2 rules which are more important.

#include <iostream>

template <typename T>
void func(T&& param) {
    param += 10; // Assuming its int
}

int main() {
    int a = 10; 

    func(a);
    std::cout << a << std::endl; // prints 20, Rule no 3

    func(std::move(a)) ; // Rule no 4
    return 0;
}


As you would have got it by now, it's because of these rules when an l-value is passed to a forwarding reference, it accepts it as a reference.

Now, you could ask, why these rules are required at all ? These rules are required for 'perfect forwarding' to work, which we will see in the next section.

Perfect Forwarding


Why is such a thing there in the first place ? Well, it is because of the dual nature of forwarding references which we had seen earlier. Due to that, basically two different types converged to become an l-value reference inside the function body. After that, there is no way (without std::forward and some template magic) to determine what was the exact type passed into the function.

Consider below example:
void do_work(??) { // What type of argument ?
}


template <typename T>
void wrapper(T&& args) {
    // do some work on argss
    do_work(args);
}

So, here you are. Inside function 'wrapper' when you call 'do_work', you want to copy 'args' if it was passed as l-value or pass it as r-value if it was passed as r-value. Without 'std::forward', you could have done some template magic (check previous blog post for the source) to get the same effect, but that is error prone and I am not even sure if it would be a practical solution.
Let's see how std::forward solves this issue.

std::forward

Like std::move, std::forward is also a function template that does nothing but casting. But unlike std::move which does an unconditional cast, std::forward does a conditional cast.

This is how the implementation of std::forward looks like in g++ 4.8.2 (Made it visually appealing).

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

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

Basically, the first function is enough for std::forward to work and from my experimentation also, the second overload never got called. So not sure why the second overload is there. In our examples, we will consider std::forward as the first function only.

Now, lets get back to our previous example and try to solve it by using std::forward.

class Test {
};

void do_work(const Test& t) { 
    Test a(t);
    //.....
}

void do_work(Test&& t) {
    // .......
}


template <typename T>
void wrapper(T&& args) {
    // do some work on argss
    do_work(std::forward<T>(args));
}

We solved the problem by creating 2 overloads of the 'do_work' function. First one accepting the argument by reference and the second one accepting the argument as r-value reference.
It's the job of the std::forward to cast it to the correct type and thereby call the correct overload.

Let's see how that is done. Consider the case of an l-value first:
Test t;
wrapper(t);

// Inside wrapper std::forward is called with template
// type as T.
// Since an l-value was passed, due to reference collapsing rules
// std::forward$lt;T> would essentially be std::forward<Test>

//So std::forward would be instantiated like

Test& &&  forward(typename std::remove_reference<Test&>::type& t) noexcept
{ return static_cast<Test& &&>(t); }

// Applying reference collapsing rules on it
Test& forward(Test& t) noexcept
{ return static_cast<Test&>(t); }

So, as you see for the above case, 'std::forward' casts it to an l-value reference and thereby calling the first 'do_work' overload.

Lets see the same for an r-value reference:
Test t;
wrapper(std::move(t));

// Inside wrapper, since an r-value was passed, due to reference collapsing rules
// std::forward<T> would essentially be std::forward<Test&&>

// So std::forward would be instantiated like this
Test&& && forward(Test&& && t) noexcept {
    return static_cast<T&& &&>(t);
}

//Applying reference collapsing rules
Test&& forward(Test&& t) noexcept {
    return static_cast<T&&>(t);
}

As you can see, this time it correctly cast it to an r-value reference, thereby making it call the second overload of the 'do_work' function.
This is the reason why std::forward is called conditional cast.


With that we finish with the basics of forwarding references and std::forward. The thing to remember is that, move is called with r-value references and forward is called with forwarding references.

Where to go from here ?

Back to your life :)
Take a break. Once you are ready to face fresh challenges, go through below resources:
1) http://thbecker.net/articles/rvalue_references/section_01.html
2) Scott Meyers "Effective Modern C++". Some examples in this post are influenced by his explanation.
3) What we have covered are the basics. There are more to move semantics based on its usage. For eg: Move constructor and its fallacies.
4) Know about move only types like std::unique_ptr, std::thread etc and how to work with them.



1 comment:

  1. Articles can discuss the impact of social media on mental health in adolescents, including cyberbullying and digital stressors. Digital Website Delhi Blogs can serve as a platform for discussing sustainable farming and agricultural practices, including organic farming and regenerative agriculture.

    ReplyDelete