Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

The concepts of lvalue and rvalue in C++ had been confusing to me ever since I started to learn C++. I did not fully understand the purpose and motivation of having these two concepts during programming and had not been using rvalue reference in most of my projects. Given most of the documentation on the topic of lvalue and rvalue on the Internet are lengthy and lack of concrete examples, I feel there could be some developers who have been confused as well.


In this blog post, I would like to introduce the concepts of lvalue and rvalue, followed by the usage of rvalue reference and its application in move semantics in C++ programming.

lvalue VS rvalue

In C++, each expression, such as an operator with its operands, literals, and variables, has type and value. We could categorize each expression by type or value. Each expression is either lvalue (expression) or rvalue (expression), if we categorize the expression by value. Note that when we say lvalue or rvalue, it refers to the expression rather than the actual value in the expression, which is confusing to some people. So personally I would rather call an expression lvalue expression or rvalue expression, without omitting the word “expression”.


lvalue expression is so-called because historically it could appear on the left-hand side of an assignment expression, while rvalue expression is so-called because it could only appear on the right-hand side of an assignment expression. lvalue expression is associated with a specific piece of memory, the lifetime of the associated memory is the lifetime of lvalue expression, and we could get the memory address of it. rvalue expression might or might not take memory. Even if an rvalue expression takes memory, the memory taken would be temporary and the program would not usually allow us to get the memory address of it.

Example

#include <iostream>

class Foo
{
public:
    Foo()
    {
    }
    Foo(int x) : mX{x}
    {
    }
    int mX = 0;
};

void printFoo1(const Foo& foo)
{
    std::cout << foo.mX << std::endl;
}

void printFoo2(const Foo foo)
{
    std::cout << foo.mX << std::endl;
}

int main()
{
    int x = 1; // rvalue expression as a whole, x is lvalue expression, 1 is rvalue expression and has no memory associations
    Foo foo1{10}; // rvalue expression as a whole, foo1 is lvalue expression
    Foo foo2{20}; // rvalue expression as a whole, foo2 is lvalue expression
    foo1 = Foo(); // rvalue expression as a whole, foo1 is lvalue expression, Foo() is rvalue expression and takes temporary memory
    foo1 = foo2; // rvalue expression as a whole, foo1 is lvalue expression
    printFoo1(foo1); // rvalue expression as a whole, printFoo1 is lvalue expression
    printFoo2(foo1); // rvalue expression as a whole, printFoo2 is lvalue expression
    std::cout << &printFoo1 << std::endl; // printFoo1 is lvalue expression and we could get the address of it
}

rvalue Reference

T& is the operator for lvalue reference, and T&& is the operator for rvalue reference. Literally it means that lvalue reference accepts an lvalue expression and lvalue reference accepts an rvalue expression.

Example

#include <string>
#include <iostream>

int main()
{
    std::string str1 = "Hello";
    std::string str2 = " Underworld";
    std::string str3 = str1 + str2;
    std::string& str_lref = str3; // Cannot do lvalue reference std::string& str_lref = str1 + str2, because str1 + str2 is rvalue expression
    std::string&& str_rref = str1 + str2; // rvalue reference
    std::string str_punc = "!";
    std::cout << &str_lref << std::endl;
    std::cout << &str_rref << std::endl;
    str_lref += str_punc; // str_lref is lvalue expression
    str_rref += str_punc; // str_rref is lvalue expression
    std::cout << str_lref << std::endl;
    std::cout << str_rref << std::endl;
}

In this particular example, at first glance, the rvalue reference seems to be useless. Why would we bother to use rvalue reference given lvalue could do the same thing. In the next section, we would see that rvalue reference is used for move semantics which could potentially increase the performance of the program under some circumstances. We would also see that only by rvalue reference we could distinguish move semantics from copy semantics.

Move Semantics

In C++, we could create a new variable from another variable, or assign the value from one variable to another variable. To keep both variables “alive”, we would use copy semantics, i.e., copy one variable to another. In some scenarios, after assigning the value from one variable to another variable, the variable that gave the value would be no longer useful, so we would use move semantics.


Because move semantics does fewer memory manipulations compared to copy semantics, it is faster than copy semantics in general. Let’s take a look at the following example.

Example

/*
 * move.cpp
 */
#include <string>
#include <iostream>
#include <vector>
#include <chrono>

class FooIncomplete
{
public:
    // Default constructor
    FooIncomplete()
    {
    }
    // Non-default constructor
    FooIncomplete(size_t num) : mNum{num}
    {
        this->mArr = new int[this->mNum];
    }
    ~FooIncomplete()
    {
        delete[] this->mArr;
    }
    // Copy constructor
    // Copy constructor has access to private members
    FooIncomplete(const FooIncomplete& foo)
    {
        std::cout << "Copy constructor called!" << std::endl;
        this->mArr = new int[foo.mNum];
        std::copy(foo.mArr, foo.mArr + foo.mNum, this->mArr);
        this->mNum = foo.mNum;
    }
    // Copy assignment
    FooIncomplete& operator=(const FooIncomplete& foo)
    {
        std::cout << "Copy assignment called!" << std::endl;
        delete[] this->mArr;
        this->mArr = new int[foo.mNum];
        std::copy(foo.mArr, foo.mArr + foo.mNum, this->mArr);
        this->mNum = foo.mNum;
    }
private:
    size_t mNum{0};
    int* mArr{nullptr};
};

class Foo
{
public:
    // Default constructor
    Foo()
    {
    }
    // Non-default constructor
    Foo(size_t num) : mNum{num}
    {
        this->mArr = new int[this->mNum];
    }
    ~Foo()
    {
        delete[] this->mArr;
    }
    // Copy constructor
    // Copy constructor has access to private members
    Foo(const Foo& foo)
    {
        std::cout << "Copy constructor called!" << std::endl;
        this->mArr = new int[foo.mNum];
        std::copy(foo.mArr, foo.mArr + foo.mNum, this->mArr);
        this->mNum = foo.mNum;
    }
    // Copy assignment
    Foo& operator=(const Foo& foo)
    {
        std::cout << "Copy assignment called!" << std::endl;
        delete[] this->mArr;
        this->mArr = new int[foo.mNum];
        std::copy(foo.mArr, foo.mArr + foo.mNum, this->mArr);
        this->mNum = foo.mNum;
    }
    // Move constructor
    Foo(Foo&& foo)
    {
        std::cout << "Move constructor called!" << std::endl;
        this->mNum = foo.mNum;
        this->mArr = foo.mArr;
        foo.mNum = 0;
        foo.mArr = nullptr;
    }
    // Move assignment
    Foo& operator=(Foo&& foo)
    {
        std::cout << "Move assignment called!" << std::endl;
        delete[] this->mArr;
        this->mNum = foo.mNum;
        this->mArr = foo.mArr;
        foo.mNum = 0;
        foo.mArr = nullptr;
    }
private:
    size_t mNum{0};
    int* mArr{nullptr};
};

inline Foo createFoo(size_t num)
{
    return Foo{num};
}

inline FooIncomplete createFooIncomplete(size_t num)
{
    return FooIncomplete{num};
}

int main()
{
    size_t num = 10000000;

    std::cout << "Foo:" << std::endl;
    std::cout << "------------------------------" << std::endl;
    Foo foo_1{num}; 
    Foo foo_2{foo_1}; // Copy constructor called!
    Foo foo_3;
    foo_3 = foo_1; // Copy assignment called!

    std::cout << "------------------------------" << std::endl;
    Foo foo_4{std::move(foo_1)}; // Move constructor called!
    Foo foo_5;
    Foo foo_6{num};
    foo_5 = std::move(foo_6); // Move assignment called!
    Foo foo_7 = std::move(Foo{num}); // Move constructor called!

    std::cout << "------------------------------" << std::endl;

    std::cout << "FooIncomplete:" << std::endl;
    std::cout << "------------------------------" << std::endl;
    FooIncomplete fooIncomplete_1{num}; 
    FooIncomplete fooIncomplete_2{fooIncomplete_1}; // Copy constructor called!
    FooIncomplete fooIncomplete_3;
    fooIncomplete_3 = fooIncomplete_1; // Copy assignment called!

    std::cout << "------------------------------" << std::endl;
    FooIncomplete fooIncomplete_4{std::move(fooIncomplete_1)}; // Copy constructor called!
    FooIncomplete fooIncomplete_5;
    FooIncomplete fooIncomplete_6{num};
    fooIncomplete_5 = std::move(fooIncomplete_6); // Copy assignment called!
    FooIncomplete fooIncomplete_7 = std::move(FooIncomplete{num}); // Copy constructor called!

    std::cout << "------------------------------" << std::endl;

    int numIter = 10;
    Foo foo;
    FooIncomplete fooIncomplete;
    std::chrono::steady_clock::time_point fooCopyBegin = std::chrono::steady_clock::now();
    for (int i = 0; i < numIter; i ++)
    {
        fooIncomplete = createFooIncomplete(num);
    }
    std::chrono::steady_clock::time_point fooCopyEnd = std::chrono::steady_clock::now();

    std::chrono::steady_clock::time_point fooMoveBegin = std::chrono::steady_clock::now();
    for (int i = 0; i < numIter; i ++)
    {
        foo = createFoo(num);
    }
    std::chrono::steady_clock::time_point fooMoveEnd = std::chrono::steady_clock::now();

    std::cout << "Foo copy assignment time: " << std::chrono::duration_cast<std::chrono::microseconds>(fooCopyEnd - fooCopyBegin).count() << "[µs]" << std::endl;
    std::cout << "Foo move assignment time: " << std::chrono::duration_cast<std::chrono::microseconds>(fooMoveEnd - fooMoveBegin).count() << "[µs]" << std::endl;
}

To compile the program, please run the following command in the terminal.

$ g++ move.cpp -o move --std=c++14

We ran the program and got the expected outputs.

$ ./move
Foo:
------------------------------
Copy constructor called!
Copy assignment called!
------------------------------
Move constructor called!
Move assignment called!
Move constructor called!
------------------------------
FooIncomplete:
------------------------------
Copy constructor called!
Copy assignment called!
------------------------------
Copy constructor called!
Copy assignment called!
Copy constructor called!
------------------------------
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Foo copy assignment time: 181627[µs]
Foo move assignment time: 44[µs]

We could see that move assignment is much faster than copy assignment! This is simply because every time we do move assignment, we just changed the value of pointers, while every time we do copy assignment, we had to allocate a new piece of memory and copy the memory from one to the other. If there are no concepts of lvalue expression and rvalue expression, we could probably only choose copy semantics or move semantics in our implementations.


Using Valgrind for C++ programs is one of the best practices. Valgrind showed there is no memory leak or error for our program.

$ valgrind --leak-check=full ./move
==2228== Memcheck, a memory error detector
==2228== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2228== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2228== Command: ./move
==2228== 
Foo:
------------------------------
Copy constructor called!
Copy assignment called!
------------------------------
Move constructor called!
Move assignment called!
Move constructor called!
------------------------------
FooIncomplete:
------------------------------
Copy constructor called!
Copy assignment called!
------------------------------
Copy constructor called!
Copy assignment called!
Copy constructor called!
------------------------------
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Copy assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Move assignment called!
Foo copy assignment time: 413274[µs]
Foo move assignment time: 930[µs]
==2228== 
==2228== HEAP SUMMARY:
==2228==     in use at exit: 0 bytes in 0 blocks
==2228==   total heap usage: 45 allocs, 45 frees, 1,720,073,728 bytes allocated
==2228== 
==2228== All heap blocks were freed -- no leaks are possible
==2228== 
==2228== For counts of detected and suppressed errors, rerun with: -v
==2228== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

We might still have one question. Class Foo could adaptively choose between move constructor/assignment and copy constructor/assignment, based on whether the expression it received it lvalue expression or rvalue expression. However, in the class FooIncomplete, there are only copy constructor and copy assignment operator which take lvalue expressions. Given a rvalue to FooIncomplete, why the copy constructor or copy assignment was invoked? Fundamentally, this is because C++ allows us to bind a const lvalue to an rvalue. For instance,

int& x = 100;       // This would raise error.
const int& x = 100; // This is fine.

If we tried to remove the const in the copy constructor and copy assignment in the Foo and FooIncomplete class, we would get the following errors, namely, it cannot bind non-const lvalue reference to an rvalue, as expected.

$ g++ move.cpp -o move --std=c++14
move.cpp: In function ‘FooIncomplete createFooIncomplete(size_t)’:
move.cpp:110:12: error: cannot bind non-const lvalue reference of type ‘FooIncomplete&’ to an rvalue of type ‘FooIncomplete’
     return FooIncomplete{num};
            ^~~~~~~~~~~~~~~~~~
move.cpp:24:5: note:   initializing argument 1 of ‘FooIncomplete::FooIncomplete(FooIncomplete&)’
     FooIncomplete(FooIncomplete& foo)
     ^~~~~~~~~~~~~
move.cpp: In function ‘int main()’:
move.cpp:141:44: error: cannot bind non-const lvalue reference of type ‘FooIncomplete&’ to an rvalue of type ‘std::remove_reference<FooIncomplete&>::type {aka FooIncomplete}’
     FooIncomplete fooIncomplete_4{std::move(fooIncomplete_1)}; // Copy constructor called!
                                   ~~~~~~~~~^~~~~~~~~~~~~~~~~
move.cpp:24:5: note:   initializing argument 1 of ‘FooIncomplete::FooIncomplete(FooIncomplete&)’
     FooIncomplete(FooIncomplete& foo)
     ^~~~~~~~~~~~~
move.cpp:144:32: error: cannot bind non-const lvalue reference of type ‘FooIncomplete&’ to an rvalue of type ‘std::remove_reference<FooIncomplete&>::type {aka FooIncomplete}’
     fooIncomplete_5 = std::move(fooIncomplete_6); // Copy assignment called!
                       ~~~~~~~~~^~~~~~~~~~~~~~~~~
move.cpp:32:20: note:   initializing argument 1 of ‘FooIncomplete& FooIncomplete::operator=(FooIncomplete&)’
     FooIncomplete& operator=(FooIncomplete& foo)
                    ^~~~~~~~
move.cpp:145:46: error: cannot bind non-const lvalue reference of type ‘FooIncomplete&’ to an rvalue of type ‘std::remove_reference<FooIncomplete>::type {aka FooIncomplete}’
     FooIncomplete fooIncomplete_7 = std::move(FooIncomplete{num}); // Copy constructor called!
                                     ~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
move.cpp:24:5: note:   initializing argument 1 of ‘FooIncomplete::FooIncomplete(FooIncomplete&)’
     FooIncomplete(FooIncomplete& foo)
     ^~~~~~~~~~~~~
move.cpp:155:44: error: cannot bind non-const lvalue reference of type ‘FooIncomplete&’ to an rvalue of type ‘FooIncomplete’
         fooIncomplete = createFooIncomplete(num);
                         ~~~~~~~~~~~~~~~~~~~^~~~~
move.cpp:32:20: note:   initializing argument 1 of ‘FooIncomplete& FooIncomplete::operator=(FooIncomplete&)’
     FooIncomplete& operator=(FooIncomplete& foo)
                    ^~~~~~~~

Conclusions

The concepts of lvalue expressions and rvalue expressions are sometimes brain-twisting, but rvalue reference together with lvalue reference gives us more flexible options for programming. Without rvalue expression, we could do only one of the copy assignment/constructor and move assignment/constructor.

References