lvalue, rvalue and their references
Understanding lvalue, rvalue, their references and const references.
1. Introduction
- An lvalue refers to an object that occupies some identifiable location in memory (it has an address). Typically, lvalues are variables or objects that you can assign values to. It represents an object that persists beyond a single expression.
(Persistent Memory Location, Identifiable Address, Modifiable)
- An rvalue on the other hand, is a temporary object that does not persist beyond the expression that uses it. Rvalues typically do not have a persistent memory location.
(Temporary Storage, No Identifiable Address, Immutable in Context)
- An lvalue reference is a reference that binds to an lvalue. Marked as
&
.1
2
3const int y{5};
int& invalidRef{y}; // invalid: can't bind to a non-modifiable lvalue
int& invalidRef2{0}; // invalid: can't bind to an rvalue
- An rvalue reference is a reference that binds to an rvalue. Marked as
&&
.
- A const lvalue reference is a reference that can bind to both
lvalues and rvalues
, providing an efficient way to access an object without modifying it.1
2
3
4
5const int y{5};
int x{3};
const int& ref1{y}; // ok, bind to a const lvalue.
const int& ref2{x}; // ok, bind to a modifiable lvalue.
const int& ref3{3}; // ok, bind to a rvalue.
2. More
2.1 Const lvalue refernce extends the lifetime of temporary objects
1 | int main() { |
2.2 Const lvalue references accept both lvalue and rvalue
1 | class LargeObject { |
2.3 Passing lvalues to rvalue reference
An rvalue reference is designed to bind to temporary objects (rvalues) that are about to be destroyed, allowing the program to safely “move” resources from those objects. However, there are situations where you might want to pass an lvalue to a function that takes an rvalue reference. This is where the std::move utility comes into play.
1 | // Function that takes an rvalue reference |
2.4 Function overloading priority
Based on what we’ve learned, both rvalue references
and const lvalue references
can accept passed rvalues. However, if we have an overloaded function with these two as arguments and we pass an rvalue, which one will the compiler call? The rule for overload resolution is: exact match > rvalue reference overload > const lvalue reference overload.
1 | void f(const int& x) { |
2.5 Constexpr lvalue references
Since constexpr
will evaluate expressions during compile time, when it applies to lvalue references, it can only bind to either global or static objects. This usage is pretty rare and has its limitations. We will not dive deep into this topic.
1 | int g_x{5}; |
3. Universal References
Universal references in C++ refer to a special type of reference that can bind to both lvalues and rvalues. This concept was introduced with C++11 and is commonly associated with template programming. Sometimes, it is also called forwarding reference.
Universal referenes are declared as T&&
in templates.
1 | template<typename T> |
The actual type of param
is determined by how function f is called.
- param is an lvalue reference if f receives an lvalue.
- param is an rvalue reference if f receives an rvalue.
Universal references are often used to implement perfect forwarding in order to preserve the lvalue\rvalue
nature of the arguments.
1 | template<typename T> |
In the following code, x
in the context of f(T&& x)
is an lvalue. This might be a little bit tricky at first sight. But whether x
is an lvalue or rvalue really depends on the context we are referring to. In the context of the caller that calls f
, for sure x
would be an rvalue reference.
If we directly call g(x)
inside f
, no matter we pass an lvalue or an rvalue to f
, eventually, void g(int& x)
will be called since x
inside f
is always an lvalue. That’s why we need perfect forwarding here to make sure g
gets exactly what passed to f
.
1 | // Overload g for lvalue references |
References
lvalue, rvalue and their references