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.
Whenever we are not sure if an expression is a rvalue object or not, we can ask ourselves the following questions.
Is it anonymous (Does it have a name?)
Is it temporary (Will it be destroyed after the expression?)
intmain() { 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.
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.
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,
1 2
int& x = 100; // This would raise error. constint& 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.