rvalue reference in C++

The rvalue reference is probably the most significant feature introduced by C++11.

Let’s quickly recap what rvalue is. It is an inherited concept from C. The expression that can appear only on the right side of an assignment statement is an rvalue. However an lvalue can appear on either side of an assignment statement. But with different types of (assignment) operator overloading, C++ made this definition incorrect. In modern C++, precise definition of rvalue is fairly complicated. But the following definition is good enough for our purpose, and for most practical purposes.

  • If we can take the memory address of an expression with ‘&’ operator, the expression is an lvalue.
  • If the type of an expression is lvalue reference, the expression is an lvalue.
  • Any other expression is an rvalue.

Before C++11 also, C++ had the concept of reference which is now called lvalue reference. The new rvalue reference can be bound to an rvalue and sometimes with lvalue also. The lvalue reference is declared with single ampersand (&) whereas an rvalue reference is declared with double ampersand (&&).

 int x = 10;
 int& ref = x;
 int&& rref = 10;

In this example, ref is an lvalue reference whereas rref is an rvalue reference. We can use both the references in a very similar way.

In fact, we can increment an integer rvalue reference just like its lvalue counterpart.

See this example.

#include <iostream>

int main() {
    int x = 10;

    int& ref = x;
    int&& rref = 20;

    std::cout << "Before increment: x: " << x << ", ref: " << ref << ", rref: " << rref << std::endl;

    ref++;
    rref++;
    
    std::cout << "After increment: x: " << x << ", ref: " << ref << ", rref: " << rref << std::endl;

    return 0;
}
$ g++ -o test test.cpp
$ ./test 
Before increment: x: 10, ref: 10, rref: 20
After increment: x: 11, ref: 11, rref: 21

Then why on earth we need an rvalue reference? Definitely not only to bind with an rvalue.

Well, the rvalue reference solves at least two very important problems – move semantics and perfect forwarding. Let’s see why the rvalue reference is required in each case in the following sections.

Move Semantics

#include <iostream>
#include <cstring>

using namespace std;

class mystring {
public:
    mystring() {
        str = nullptr;
    }
    
    mystring(const char * s) {
        if (s == nullptr) {
            s = nullptr;
            return;
        }
        str = new char[strlen(s)];
        strcpy(str, s);
    }

    ~mystring() {
        if (str) delete str;
    }

    /*Copy Assignment Operator*/
    void operator= (const mystring &rhs) {
        cout << "Copy assignment operator is called." << endl;
        if (str != nullptr) delete str;
        
        if (rhs.str == nullptr) {
            str = nullptr;
            return;
        }

        str = new char[strlen(rhs.str)];
        strcpy(str, rhs.str);
    }

private:
    char *str;
};

mystring getstring() {
    mystring s1("QnA Plus");
    return s1;
}

int main() {
    
    mystring s2;
    s2 = getstring();

    return 0;
}
$ g++ -o test test.cpp 
$ ./test 
Copy assignment operator is called.

We overloaded the assignment operator for the mystring class. This overloaded operator function will be called in time of assignment operation from one mystring object to another.

It allocates memory to store its own value (str) and copies the value from the source object. In general, this is a good thing to do. Both the source and destination objects will have their own copies of their resources. Changing one object will not impact the other.

Now consider this statement s2 = getstring(); . The copy assignment operator is called when s2 was assigned from a returned object of getstring(). The returned object is a temporary one (rvalue). We don’t care whether the returned object retains its resources after the assignment operation. Then, why to do all these expensive memory operations – allocation and copies. We can directly assign the source pointers to the destination object’s members.

But modifying the current overloaded function (operator=) is not a good idea because we need it for other type assignments.

Another Assignment Operator

So, we want another type of assignment operator that will not do these memory operations. It will do shallow copy instead. It is called move semantics.

But what will be the type of the input of that new overloaded function? It must be a reference type. Otherwise, one more copy will be involved. We already used the reference type of the copy assignment operator.

That’s where the rvalue reference comes into the picture. The input type will be rvalue reference for the new overloaded assignment function. Lets call it the move assignment operator.

The signature will look like void operator= (mystring&& rhs). We’ll simply copy the pointers (shallow copy) instead of memory allocation and copy the content. This mechanism is also know as efficient resource transfer. Similar way, we can add move constructor also.

If we add the move assignment operator and constructor to the mystring class, the s2 = getstring(); will cause those to be called.

#include <iostream>
#include <cstring>

using namespace std;

class mystring {
public:
    mystring() {
        str = nullptr;
    }
    
    mystring(const char * s) {
        if (s == nullptr) {
            s = nullptr;
            return;
        }
        str = new char[strlen(s)];
        strcpy(str, s);
    }

    /*The move constuctor*/
    mystring(mystring&& s) {
        cout << "Move construtor called." << endl;
        str = s.str;
        s.str = nullptr;
    }

    ~mystring() {
        if (str) delete str;
    }

    /*Copy Assignment Operator*/
    void operator= (const mystring &rhs) {
        cout << "Copy assignment operator is called." << endl;
        if (str != nullptr) delete str;
        
        if (rhs.str == nullptr) {
            str = nullptr;
            return;
        }

        str = new char[strlen(rhs.str)];
        strcpy(str, rhs.str);
    }

    /*Move Assignment Operator*/
    void operator= (mystring&& rhs) {
        cout << "Move assignment operator is called." << endl;
        if (str != nullptr) delete str;
        
        str = rhs.str;
        rhs.str = nullptr;
    }

private:
    char *str;
};

mystring getstring() {
    mystring s1("QnA Plus");
    return s1;
}

int main() {
    
    mystring s2;
    s2 = getstring();

    return 0;
}
$ ./test 
Move assignment operator is called.

To make a class movable, we need to add both move constructor and move assignment operator.

Another important thing, it is not that the source object has to be an rvalue. We can force move an lvalue object using std::move to another object.

Perfect Forwarding

Let’s first understand the forwarding problem.

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

////////////////////////////////////
int x = 10;
equivalent(x);
equivalent(30);

We have two versions of the overloaded function equivalent(). The equivalent(x) call will rightly be resolved to equivalent(int& a). And the equivalent(30) will be resolved to equivalent(const int& a).

Now, say, we want to have one wrapper template function that will internally call the right version of this overloaded function.

template <typename T>
void wrapper(T& a) {
    equivalent(a);
}
wrapper(x);
wrapper(30);

You might expect that the above two calls will deduce appropriate type for ‘T’ and produce the expected result. But wrapper(30) will produce compilation error. This is because ‘T&’ is considered as non-const reference. So, ‘a‘ can not be bound to an rvalue.

We can change the type to ‘const T&‘. Const reference can be bound with lvalue or rvalue.

template <typename T>
void wrapper(const T& a) {
    equivalent(a);
}

Now, there will be no compilation error. But both the calls, wrapper(x) and wrapper(30), will result into calling single version of equivalent()equivalent(const int& a). As ‘x‘ is an lvalue, the expectation is to call equivalent(int& a) for wrapper(x).

Now the solution to this problem is to have two wrapper functions.

template <typename T>
void wrapper(T& a) {
    equivalent(a);
}

template <typename T>
void wrapper(const T& a) {
    equivalent(a);
}

You’ll get expected result after this.

But for one parameter, it’s still okay to have two wrapper functions. It will be quite unmanageable if we have more number of parameters. For N parameters, we’ll have N2 combinations.

It’s much better to have a reference type that can be bound with both lvalue and rvalue. The rvalue reference is the solution. An rvalue reference with a deduced type, like, ‘T&&‘, along with std::forward will solve the problem in a much better way.

#include <iostream>
using namespace std;

void equivalent(int& a) {
    cout << "int&: " << a << endl;
}

void equivalent(const int& a) {
    cout << "const int&: " << a << endl;
}

template <typename T>
void wrapper(T&& a) {
    equivalent(forward<T>(a));
}

int main() {
    int x = 10;
    wrapper(x);
    
    wrapper(30);
    return 0;
}
$ ./test 
int&: 10
const int&: 30

The perfect forwarding problem and its solution are explained in detail in this article.

Author: Srikanta

I write here to help the readers learn and understand computer programing, algorithms, networking, OS concepts etc. in a simple way. I have 20 years of working experience in computer networking and industrial automation.


If you also want to contribute, click here.

Leave a Reply

Your email address will not be published. Required fields are marked *

0
0
0
0
0
0