R-Value References in C++11

R-Value References in C++11

In our previous article, we discussed the differences between L-Value and R-Value in C++ with some examples. In this tutorial, we will introduce L-Value References and R-Value References and explain the exact difference between them.

So, let’s start with L-Value References and then we will move toward R-Value References.

What is L-Value Reference (T&) in C++? #

Before C++11, we already had references in C++ which are specifically lvalue references (T&). Basically, an L-Value reference binds to an object with a name and a memory address, i.e., an L-Value. Here, by L-Values, we mean regular variables or objects that persist in memory and whose address can be taken. For example:

int x = 10;

// `ref` is an L-Value reference to `x`
int& ref = x;  

In this case, ref is an lvalue reference that refers to the variable x because x is an L-Value and it persists in memory. L-Value references are very useful because they let us avoid copying large objects. Instead of passing a large object by value (which creates a copy) to a function, we can pass it by reference, which is faster and more memory-efficient. Let’s understand this with an example:

Passing Object by Value #

When you pass an object by value, a copy of the object is created. This means the original object is duplicated, and all the member variables are copied into the new object. Depending on the size of the object, this can be quite expensive in terms of both time and memory.

For example, here we pass the Student object by value:

// Display function accepting Student by value
void Display(Student obj)
{
    // Some code is here...
}

Student s1("Mark", 22);

// Pass by value, copies s1
Display(s1);  

When we call the Display() function with the Student object s1, the copy constructor of the Student class will be invoked because we are passing the Student object by value. This can lead to performance overhead.

Passing Object by Reference #

To avoid the overhead of copying, we can pass the object by reference. This allows the function to work directly with the original object without making any copies.

For example, here we have a function Display() that accepts a Student object as a reference. So, we will call the Display() function and pass the Student object by reference to it.

// Display function accepting Student by refernce
void Display(Student & obj)
{
    // Some code is here...
}

Student s2("Sumit", 33);

// Pass by reference, so no copy
Display(s2);  

Here, when we invoked the Display() function, we passed the Student object by reference. So, NO Copy of the object is created! Instead, we operate directly on the original object (s2), avoiding the overhead of copying.

This old-style reference is called an L-Value Reference, and it has been part of C++ since the very beginning, allowing us to modify objects by reference and pass objects into functions without unnecessary copying.

Now, L-Value References refer to L-Values; similarly, R-Value references were introduced in C++ to refer to R-Values. Let’s have a quick refresher on R-Value and L-Value.

Lvalue vs Rvalue in C++ #

  • L-Values are objects that persist in memory and have an identifiable address. For example, in the expression int x = 10;, the variable x is an L-Value because it has a name and an address.

  • R-Values, on the other hand, are temporary values, essentially values that exist only for a moment, like the result of an expression (x + 5). They don’t have a specific memory location, and you cannot take the address of x + 5.

Now, prior to C++11, you could not store a reference to R-Values. But from C++11 onwards, you can create references to R-Values using R-Value References (T&&). Let’s learn about them.

Rvalue References (T&&) in C++11 #

With the introduction of C++11, we got a new kind of reference: the rvalue reference. The Rvalue references are designed to bind to those temporary, unnamed values (R-Values) that regular lvalue references cannot touch.

Syntax of Rvalue References (T&&) #

Rvalue references use the syntax T&&, where T is the type of the object. They allow you to refer to temporary objects (rvalues) and operate on them without making copies. This is particularly useful for improving performance, which we will demonstrate with an example very soon.

Let’s first explore some basic examples of R-Value references:

// Bind R-Value Reference to a R-Value
int&& ref = 10;  

In this example, 10 is an R-Value, and ref is an rvalue reference which refers to the temporary value 10. Notice that 10 is an rvalue because it’s just a literal that does not have a permanent memory location. This is where rvalue references improve performance: they allow us to efficiently ‘capture’ and work with temporary values.

You cannot bind an L-Value reference to a temporary value, i.e., an R-Value. For example,

// Error: Can not create L-Value reference to temporary value 10
int & ref2 = 10; 

Also, you cannot bind an R-Value reference to an L-Value. For example, if you try to bind an rvalue reference ref3 to an lvalue x, it will result in an error.

int x = 5;

// Error: Cannot bind rvalue reference to an lvalue
int&& ref3 = x;  

Rvalue references are meant to deal with temporary objects, so you can bind an R-Value reference to a temporary value only. For example,

int && ref4 = 10;

int y = 8;

int && ref5 = y + 7;

Here, the R-Value reference ref4 binds to a temporary value 10. Also, the R-Value reference ref5 binds to a temporary value y + 7.

L-Value Reference (T&) vs R-Value Reference (T&&) #

The key difference between these two types of references lies in what they bind to:

  • T& (L-Value Reference): Binds to lvalues, which are persistent objects with a name and address in memory. L-Value references allow us to avoid copying and directly modify or access the referred object.

  • T&& (R-Value Reference): Binds to rvalues, which are temporary objects or the results of expressions. R-Value references enable us to move resources from one object to another efficiently, instead of creating unnecessary copies.

R-Value References were introduced in C++11 because they provide a performance boost. Let’s understand that with examples.

Example 1 of R-Value References #

Suppose we have a function DisplayData(),

void DisplayData(int & a)
{
    std::cout << a * 5 << std::endl;
}

It accepts an integer as an l-value reference as an argument, multiplies it by 5, and prints the value. It does not modify the referenced value it receives as a parameter.

Now, if we invoke the DisplayData() function with an l-value like this,

int y = 10;

// Pass a L-Value to function
DisplayData(y);

Here, the variable y is an L-Value, and when it is passed as a parameter to the function DisplayData(int & a), a l-value reference a will be created inside the function, and we can use it within the function.

But what if we pass a temporary value like 222, i.e.,"

// Pass a R-Value (Temporary Value) to function
DisplayData(222);

This will throw a compile error because the DisplayData(int & a) function will try to create an l-value reference, but we are passing an r-value here. Yes, 222 is an r-value, and we cannot create an l-value reference to an r-value. So, this will not work. The old solution would be to create a function that accepts an int by value i.e.

DisplayData(int a);

But this will lead to poor performance because we will be creating an unnecessary copy here. In the case of large objects instead of int, the copy constructor will be called, causing inefficiency and leading to poor performance.

However, with C++11, we can create an overloaded version of this function that accepts an r-value reference.

// Accepts R-Value Reference as parameter
void DisplayData(int && a)
{
    std::cout << a * 5 << std::endl;
}

Now, if we call the DisplayData() function with a temporary value, i.e., an r-value like 222,

// Pass a R-Value (Temporary Value) to function
DisplayData(222);

The overloaded version: DisplayData(int && a) will be called here, where we are accepting an r-value reference. So, no copy will be created because we bind to temporary values using the r-value reference.

When we pass a l-value to this function like this,

int x = 6;

// Pass a L-Value to function
DisplayData(x);

Then the pverloaded version: DisplayData(int & a) that accepts an l-value reference will be called. In this case, no copy will be created.

R-Value references Boosts the Performance

Using r-value references, we can bind to temporary values and avoid unnecessary copying of temporary objects. It might look simple here, but with large objects containing big resources as member variables, copying becomes a significant operation, and avoiding unnecessary copying of temporary objects in such cases gives a substantial performance boost.

Example 2 of R-Value References #

Let’s see another example.

void DisplayString(std::string & data)
{
    std::cout << "Received L-Value Reference" << std::endl;
    std::cout << data << std::endl;
}

Here we have another function DisplayString(), which accepts a string as an L-Value reference in the parameter. Now, if we call this with a temporary string like this:

DisplayString("Another Text"); // Error

It will give an error because we passed a temporary string as an argument to the DisplayString(std::string & data) function, and it’s an r-value. We cannot create an l-value reference to an r-value (temporary object), so it will result in an error. To handle this, we could either overload the DisplayString() function to accept a string by value, like this:

void DisplayString(std::string data);

This will work, but it will create a copy of the string from the temporary object. To avoid the unnecessary creation of an object from a temporary, we can overload the DisplayString() function with an r-value reference, like this:

void DisplayString(std::string && data)
{
    std::cout << "Received R-Value Reference" << std::endl;
    std::cout << data << std::endl;
}

Now, if we call the function with a temporary string like this:

DisplayString("Another Text");

The overloaded version void DisplayString(std::string && data) will be called because we passed an r-value, and the function that accepts an r-value reference will be invoked. If we call the DisplayString() function with an l-value like this:

std::string str = "Some text";
DisplayString(str);

The overloaded version of DisplayString() that accepts an L-Value reference, i.e., DisplayString(std::string & data), will be called. This way, we can avoid copying temporary objects by using r-value references in C++."

L-Value references (T&) and r-value references (T&&) are essential tools in C++ that allow us to manage resources efficiently. While l-value references help us avoid unnecessary copying of objects that persist in memory, r-value references allow us to bind references to temporary objects and improve performance.

Summary #

By using r-value references with move semantics, you can significantly reduce memory usage and CPU cycles, making your C++ programs faster and more efficient. So go ahead, start practicing with these concepts, and unlock the full potential of modern C++!

In the next tutorial, we will see an example with a large and heavy class, and we will implement a move constructor in that class along with a copy constructor using r-value references to improve performance when working with temporary objects and STL containers. This will have a big impact on the performance of your applications.

Author: Varun

A Software Developer with 20 Years of Experience in C/C++