Rvalue References and Perfect Forwarding

Discussion in 'C++' started by BiplabKamal, Feb 10, 2016.

  1. We know that C++11 added the feature of rvalue reference. Here assumption is that the reader is already familiar with rvalue and lvalue and their references. As we know that we declare rvalue reference with double ampersand like:

    int&& var1 = 10; // var1 is an rvalue reference variable means it needs to be initialized with an rvalue

    Whereas for lvalue reference:

    int i;
    int& var1 = i; // var1 is an lvalue reference variable means it needs to be initialized with an lvalue
    const int& var1 = 10;// var1 is an const lvalue reference means it needs to be initialized with an lvalue or rvalue

    What about the following decelerations where automatic type deduction by compiler is involved?
    Code:
    auto&& var1 = 100; // var1 is rvalue reference as it is declared with && and the initialization parameter is an rvalue
    int i =0; // i is an lvalue
    auto&& var2 = i; // Though declared with &&, var2 is not an rvalue reference, it is an lvalue reference because it is initialized with lvalue
    auto&& var3 = std::move(i); // var3 is an rvalue reference because it is declared with && and initialized with rvalue
    const int ci = 10; // ci is an constant lvalue
    auto&& value = ci; //value is a constant lvalue reference as it is declared with && but initialized with constant lvalue
    
    So declaring with && does not always make a variable or parameter as rvalue reference when automatic type deduction occurs. That is because of type deduction rules. T&& is always rvalue reference if T is a specified type. Type deduction happen while initializing auto type variable or calling template function without specifying the template arguments. So T&& has two meanings, in one case it is always rvalue reference and in another case it is either rvalue or lvalue reference depending upon the initialization object. When the second meaning is in force, it is also called universal reference. In the following example of function template you can pass either lvalue or rvalue as the parameter while calling the function:
    Code:
    template<typename T>
    void MyFn(T&& obj)
    {
        if (std::is_rvalue_reference<T&&>())
            cout << " MyFn(T&& )is called" << endl;
        else
            cout << " MyFn(T&) is called" << endl;
    }
    int main()
    {
        MyFn(10);// template function instance called:  void MyFn(int&& obj). Here T is deduced to be int
        int i;
        MyFn(i);// template function instance called:  void MyFn(int& obj).Here T is deduced to be int& and reference collapsing applies
    }
    
    Output:
    Code:
     MyFn(T&& )is called
     MyFn(T&) is called
    
    So usage of Rvalue reference looks some what complicated when combined with automatic type deduction. There could be unintended behavior when function template arguments are declared with && thinking it will be rvalue reference only. Apparently the impact seems to be only upto the calling of the function because inside a function parameters are always lvalues irrespective of whether they are passed as lvalue or rvalue and also does not matter if they are passed by value or reference. But there is an issue when the template function receives parameters by rvalue reference and wants to forward it's parameters to another function as rvalue reference. Take the following example:
    Code:
    #include<iostream>
    using namespace std;
    template<typename T>
    void Process(T&& obj)
    {
        if (std::is_rvalue_reference<T&&>())
            cout << " Process(T&& )is called" << endl;
        else
            cout << " Process(T&) is called" << endl;
        
    }
    
    template<typename T>
    void LogAndProcess(T&& obj)
    {
        if (std::is_rvalue_reference<T&&>())
            cout << " LogAndProcess(T&& )is called" << endl;
        else
            cout << " LogAndProcess(T&) is called" << endl;
        Process(obj);
    }
    class AnyClass
    {
    
    };
    int main()
    {
        LogAndProcess(AnyClass());
    }
    
    Output:
    Code:
     LogAndProcess(T&& )is called
     Process(T&) is called
    
    Here the wrapper function LogAndProcess() is called with rvalue but when it calls target function Process(), lvalue is passed.This is because inside the function parameters are lvalues. If we want to forward the wrapper's parameter to the target function as it is there is no way except type casting to rvalue. Here two scenarios- In one scenario we pass lvalue to the Wrapper function and in another scenario we pass rvalue. In first case we don't need to do anything but in 2nd case we need to type cast back to rvalue. This conditional typecasting is called perfect forwarding. std::forward template function does this conditional type casting. Now look into code which does the forwarding-
    Code:
    template<typename T>
    void Process(T&& obj)
    {
        if (std::is_rvalue_reference<T&&>())
            cout << " Process(T&& )is called" << endl;
        else
            cout << " Process(T&) is called" << endl;
    }
    
    template<typename T>
    void LogAndProcess(T&& obj)
    {
        if (std::is_rvalue_reference<T&&>())
            cout << " LogAndProcess(T&& )is called" << endl;
        else
            cout << " LogAndProcess(T&) is called" << endl;
        Process(std::forward<T>(obj));
    }
    class AnyClass
    {
    
    };
    int main()
    {
        LogAndProcess(AnyClass()); // T is deduced as AnyClass
        AnyClass obj;
        LogAndProcess(obj);// T is deduced as AnyClass&    
    }
    
    Output:
    Code:
     LogAndProcess(T&& )is called
     Process(T&& )is called
     LogAndProcess(T&) is called
     Process(T&) is called
    
    std::forward() does not forward but type cast the argument to rvalue only if it is bound to an rvalue. In the above example if we call LogAndProcess() passing rvalue expectation is that it should call the Process() with rvalue. On the other hand if we call LogAndProcess() with lvalue it should call the Process() with lvalue. But we know that the parameter obj is always lavalue inside LogAndProcess(). Then how do we pass it as rvalue to the function Process(). So we need to do conditional cast i.e. when obj is bound to an rvalue cast obj to rvalue, otherwise do not cast. Now how do we know whether the obje is bound to lvalue or rvalue? This information is available with the template argument T. T is passed to std::forward() as template argument and it can retrieves the encoded information. Here is how T is deduced in different situations:

    - When lvalue is passed T is deduced to be lvalue references
    - When rvalue is passed T is deduced to be non-reference
    Code:
    std::forward() function implementation looks like:
    template<class T>
    T&& forward(typename std::remove_reference<T>::type& t) noexcept
    {
        return static_cast<T&&>(t);
    }
    
    So when we call LogAndProcess() with lvalue. Like LogAndProcess(AnyClass()); in the example, the T is deduced to AnyClass& and std::forward() instance looks like:
    Code:
    AnyClass& && forward(typename std::remove_reference<AnyClass&>::type& t) noexcept
    {
        return static_cast<AnyClass& &&>(t);
    }
    
    Applying reference collapsing rule and resolving std::remove_reference<AnyClass&>::type the final version of the std::forward() is:
    Code:
    AnyClass& forward(AnyClass& t) noexcept
    {
        return static_cast<AnyClass&>(t);
    }
    
    The above function takes a lvalue reference and returns lvalue reference. This means it does not do any thing.

    Now look into the other scenario when we call LogAndProcess() with rvalue. Like LogAndProcess(obj); in the example, the T is deduced to AnyClass and std::forward() instance looks like:
    Code:
    AnyClass&& forward(typename std::remove_reference<AnyClass>::type& t) noexcept
    {
        return static_cast<AnyClass&&>(t);
    }
    
    Applying std::remove_reference to non- reference yields to the same type. So the final version of std::forward() is :
    Code:
    AnyClass&& forward(AnyClass& t) noexcept
    {
        return static_cast<AnyClass&&>(t);
    }
    
    Note that there is no reference collapsing required as there is no reference to reference here.In this case forward will return an rvalue. So the result is that an ravalue passed to LogAndProcess() will be passed to Process() as rvalue which is our intention.

    Perfect forwarding is used in many standard library template classes. For example std::make_unique and std::make_shared are template functions which use std:forward to pass the argument objects to smart pointer's constructor
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice