MiHoYo C++ Interview: The Difference Between Lvalue References and Rvalue References? The Significance of Rvalue References?

In C++, a “reference” is an indirect way to access a variable, and Lvalue Reference and Rvalue Reference are two core reference types introduced in C++11. Lvalue references, as traditional reference types, have long been widely used in scenarios such as function parameter passing and return value optimization; Rvalue references were created to address issues like “temporary object copy overhead” and “implementation of move semantics”. They are the foundation of modern C++ (C++11 and later) features such as move semantics and perfect forwarding. This article will start with the “essential distinction between lvalues and rvalues” and systematically compare the differences between lvalue references and rvalue references, deeply analyzing the core significance of rvalue references, and providing code examples to help developers master this key technology in modern C++.

1. Preliminary Concepts: Distinguishing Between Lvalues (Lvalue) and Rvalues (Rvalue)

To understand lvalue references and rvalue references, it is first necessary to clarify the definitions of “lvalue” and “rvalue”—the core difference between the two lies in “whether they can be addressed” and “whether they have persistence”.

1. Lvalue (Lvalue): Addressable “Persistent Object”

Lvalues refer to objects or variables that have a memory address and can be accessed in multiple statements, characterized by “persistence” (longer lifecycle, not destroyed immediately after a single expression ends).

Common lvalue scenarios:

  • Variables (including ordinary variables, global variables, class member variables);
  • Array elements, pointers (pointers to specific objects);
  • Function calls returning lvalue references (int& func());
  • Dereference expressions (such as*ptr), prefix increment/decrement expressions (such as++i which return the variable itself).

Example: Lvalue Behavior

int a = 10;          // a is an lvalue (has an address, can be accessed long-term)int arr[5] = {1,2,3};// arr[0], arr[1], etc. are lvalues (array elements have addresses)int* ptr = &a;       // ptr is an lvalue (pointer variable has an address)*ptr = 20;           // *ptr is an lvalue (dereferencing points to a, has an address)++a;                 // ++a is an lvalue (returns a itself, can continue to assign: ++a = 30;)

2. Rvalue (Rvalue): Non-addressable “Temporary Object”

Rvalues refer to objects or values that do not have a persistent memory address, are only valid within a single expression, and will be destroyed after the expression ends, characterized by “temporariness”.

Common rvalue scenarios:

  • Literal constants (such as10,3.14,“hello”);
  • Temporary results of expressions (such asa + b,i++,i++ returns the temporary value before increment);
  • Function calls returning non-reference types (such asint func(), which returns a temporary copy);
  • Anonymous objects (such asPerson(), instances of classes not received by variables).

Example: Rvalue Behavior

int a = 10;10;                  // 10 is an rvalue (literal constant, no address)a + b;               // a + b's result is an rvalue (temporary calculation result, no address)i++;                 // i++ is an rvalue (returns the temporary value before increment, cannot be assigned: i++ = 30; error)func();              // if func returns int, its result is an rvalue (temporary copy)Person("Alice", 20); // Anonymous object is an rvalue (not received by a variable, destroyed after expression ends)

Key Distinction Techniques

  • Can it be addressed with& : Lvalues can be addressed (&a is legal), rvalues cannot be addressed (&10,&(a + b) are illegal);
  • Can it be placed on the left side of the assignment operator: Lvalues can be placed on the left side of= (a = 20 is legal), rvalues cannot be placed on the left side of= (10 = a,(a + b) = 20 are illegal).

2. Core Differences Between Lvalue References and Rvalue References

Lvalue references (denoted by& ) and rvalue references (denoted by&& ) are “exclusive references” for lvalues and rvalues, and there are essential differences in syntax definition, binding objects, and lifecycle.

1. Syntax Definition and Binding Rules

This is the most intuitive difference—lvalue references can only bind to lvalues, while rvalue references can only bind to rvalues (by default, C++11 allows rvalue references to bind to “expiring values”, which will be mentioned later).

(1) Lvalue Reference (T& ): Only binds to lvalues

The syntax for lvalue references is type& reference_name = lvalue_object , with the core rule being “can only bind to addressable lvalues”, and cannot bind to rvalues.

Example: Binding of Lvalue References

int a = 10;int& ref_a = a;      // Legal: ref_a is an lvalue reference, binding to lvalue aint& ref_b = 10;     // Error: 10 is an rvalue, lvalue reference cannot bind to rvalueint& ref_c = a + b;  // Error: a + b is an rvalue, lvalue reference cannot bindint& ref_d = func(); // Error: if func returns int (non-reference), the result is an rvalue

Exception Scenario:constt Lvalue references (const T& ) can bind to rvalues

To support “receiving temporary objects with references” (such as function parameters receiving literals), C++ allowsconstt modified lvalue references to bind to rvalues, at which point the rvalue’s lifecycle will be extended to match that of the reference. This is a common technique for handling temporary objects in C++98/03.

const int& ref_a = 10;     // Legal: const lvalue reference can bind to rvalue 10const int& ref_b = a + b;  // Legal: const lvalue reference can bind to expression temporary resultconst Person& ref_c = Person("Alice"); // Legal: const lvalue reference extends the lifecycle of the anonymous object

(2) Rvalue Reference (T&& ): Only binds to rvalues

The syntax for rvalue references is type&& reference_name = rvalue_object , with the core rule being “can only bind to rvalues (temporary objects, literals, etc.)”, and cannot bind to lvalues (by default).

Example: Binding of Rvalue References

int a = 10;int&& ref_a = 10;     // Legal: ref_a is an rvalue reference, binding to rvalue 10int&& ref_b = a + b;  // Legal: ref_a binds to the expression temporary resultint&& ref_c = func(); // Legal: if func returns int, the result is an rvalue, can bind to rvalue referenceint&& ref_d = a;      // Error: a is an lvalue, rvalue reference cannot directly bind to lvalues

Exception Scenario: : Throughstd::move to convert lvalues to rvalue references

std::move is a template function provided in C++11, which serves to “force convert an lvalue to an rvalue reference type” (essentially telling the compiler “this lvalue can be moved, and will not be used afterwards”), at which point the rvalue reference can bind to the converted lvalue.

int a = 10;int&& ref_d = std::move(a); // Legal: std::move(a) converts a to rvalue reference, can bind // Note: a is still an lvalue, but should not be used afterwards (its resources may have been moved)

2. Lifecycle Impact

Lvalue references and rvalue references have completely different impacts on the lifecycle of the bound objects, which is one of the most critical differences in practical applications.

(1) Lvalue Reference: Does not affect the lifecycle of lvalues

Lvalue references bind to “existing lvalues” (such as variablesa), and the reference itself is merely an “alias” for the lvalue, which does not change the lifecycle of the lvalue—the lifecycle of the lvalue is determined by its own scope (e.g., local variables are destroyed after the function ends), regardless of whether the reference exists.

void func() {    int a = 10;       // a's lifecycle: destroyed after func function ends    int& ref_a = a;   // ref_a is an alias for a, does not affect a's lifecycle    ref_a = 20;       // Modifying ref_a is equivalent to modifying a} // Function ends, a is destroyed, ref_a also becomes invalid

(2) Rvalue Reference: Extends the lifecycle of rvalues

Rvalue references bind to “temporary rvalues” (such as10,a + b), and by default, rvalues will be destroyed after the expression ends; however, when an rvalue is bound by an rvalue reference, its lifecycle will be extended to match that of the rvalue reference—during the existence of the rvalue reference, the rvalue will not be destroyed.

void func() {    // 10 is an rvalue, bound by rvalue reference ref, lifecycle extended to func function end    int&& ref = 10;       ref = 20;         // Can modify rvalue reference (rvalue itself is modifiable, like temporary variables)} // Function ends, ref becomes invalid, the bound rvalue is only destroyed afterwards

This feature is the basis for rvalue references to implement “move semantics”—by extending the lifecycle of temporary objects, ensuring that move operations (such as resource transfers) can be completed safely.

3. Differences in Usage Scenarios

Lvalue references and rvalue references have different design goals, leading to clear distinctions in their applicable scenarios.

Reference Type

Core Usage Scenarios

Example Scenarios

Lvalue Reference

1. Function parameter passing (avoiding copies, modifying the original object); 2. Function returning lvalues (returning variable aliases, avoiding copies); 3. As class member references (associating external objects).

1. void swap(int& a, int& b) (modifying original variables); 2. int& getElement(int arr[], int idx) (returning array element alias); 3. Class member string& name_ (associating external strings).

Rvalue Reference

1. Implementing move semantics (transferring resources of temporary objects, avoiding copies); 2. Implementing perfect forwarding (passing parameters’ original types, used for templates); 3. Receiving temporary objects (extending lifecycle, avoiding copies).

1. Move constructor Person(Person&& other); 2. Perfect forwarding function template <typename T> void forwardFunc(T&& arg); 3. Receiving function returning temporary object Person&& p = createPerson().

4. The Core Significance of Rvalue References: Solving “Copy Overhead” and “Semantic Loss”

Before C++11, lvalue references (especiallyconstt lvalue references) could receive temporary objects but could not solve the problem of “temporary object copy overhead”—when temporary objects contain heap memory (such asstd::string , std::vector), the copy operation consumes a lot of resources (allocating memory, copying data). The emergence of rvalue references is precisely to fill this gap, and its core significance is reflected in the two major features of “move semantics” and “perfect forwarding”.

1. Significance One: Implementing Move Semantics (Move Semantics), Eliminating Temporary Object Copy Overhead

“Move semantics” refers to “transferring the resources of one object (such as heap memory, file handles) to another object, rather than copying resources”—for temporary objects (rvalues), their resources will be destroyed after the expression ends, so “moving” is more efficient than “copying” (no need to allocate new memory, just transfer pointer references). Rvalue references are the “carrier” of move semantics, binding temporary objects through rvalue references to achieve safe resource transfers.

Problem Background: Traditional Copy Overhead

For example, withstd::vector , when we initialize a new object with a temporary object, the traditional copy constructor performs a “deep copy” (allocating new memory, copying all elements), even if the temporary object will be destroyed immediately, this copy overhead cannot be avoided:

#include &lt;vector&gt;using namespace std;int main() {    // createVector returns a temporary vector (rvalue)    vector&lt;int&gt; v1 = createVector();     // Traditional copy constructor: v1 copies all elements of the temporary object, the temporary object is subsequently destroyed    // Copy overhead is large (if the vector contains 1 million elements, it needs to allocate memory of size 1 million and copy)    return 0;}

Solution: Move Constructor (Implemented with Rvalue References)

By defining a “move constructor” with rvalue references, receiving temporary objects (rvalues), directly transferring their resources (such as heap memory pointers), avoiding copies:

class MyVector {private:    int* data_; // Heap memory pointer    size_t size_;public:    // Normal constructor    MyVector(size_t size) : size_(size) {        data_ = new int[size]; // Allocate heap memory    }    // Copy constructor (lvalue reference parameter, deep copy)    MyVector(const MyVector& other) : size_(other.size_) {        data_ = new int[size_];         for (size_t i = 0; i &lt; size_; i++) {            data_[i] = other.data_[i]; // Copy data (overhead is large)        }    }    // Move constructor (rvalue reference parameter, resource transfer)    MyVector(MyVector&& other) : data_(other.data_), size_(other.size_) {        // Key: Set the source object's pointer to null, to avoid releasing memory when the source object is destructed        other.data_ = nullptr;        other.size_ = 0;    }    // Destructor    ~MyVector() {        delete[] data_; // Only release memory when data_ is non-null    }};int main() {    // createMyVector returns a temporary object (rvalue), triggering the move constructor    MyVector v1 = createMyVector();     // No deep copy: v1 directly acquires the heap memory of the temporary object, overhead is minimal    return 0;}

The Core Value of Move Semantics:

  • For objects containing a large amount of resources (heap memory, file handles, network connections), the overhead of move operations is much smaller than that of copy operations (only need to modify pointer references, no need to copy data);
  • Eliminating “meaningless copies” of temporary objects—temporary objects are destined to be destroyed, transferring their resources to new objects is more efficient.

2. Significance Two: Implementing Perfect Forwarding (Perfect Forwarding), Solving Template Parameter Passing Issues

“Perfect forwarding” refers to “in template functions, passing parameters unchanged to other functions, maintaining both the left value/right value properties of the parameters and theirconstt properties”. Before C++11, template functions could not achieve perfect forwarding (lvalues would be converted to rvalues,constt properties would be lost); rvalue references combined withstd::forward can perfectly solve this problem.

Problem Background: Traditional Template Forwarding Defects

In traditional template functions, parameter passing would lose left value/right value properties—regardless of whether an lvalue or rvalue is passed, the template parameter would be treated as an lvalue, leading to incorrect matching of target function’s left value/right value parameters:

#include &lt;iostream&gt;using namespace std;// Target function: receives lvalue reference and rvalue referencevoid process(int& x) {    cout &lt;&lt; "Processing lvalue:" &lt;&lt; x &lt;&lt; endl;}void process(int&& x) {    cout &lt;&lt; "Processing rvalue:" &lt;&lt; x &lt;&lt; endl;}// Traditional template function: cannot perfectly forward parameterstemplate &lt;typename T&gt;void forwardFunc(T arg) {    process(arg); // arg is an lvalue (template parameter is value-passed, arg is a local variable), always calls process(int&)}int main() {    int a = 10;    forwardFunc(a);    // Passing lvalue, expected to call process(int&) → actually calls correctly    forwardFunc(20);   // Passing rvalue, expected to call process(int&&) → actually calls process(int&) (error)    return 0;}

Solution: Rvalue References +std::forward to achieve perfect forwarding

Utilizing the “rvalue reference template parameter deduction rules” (i.e., “universal references”,T&& can match both lvalues and rvalues in templates), combined withstd::.forward(which converts parameters to lvalue or rvalue references based on their original types), achieving perfect forwarding:

template &lt;typename T&gt;void forwardFunc(T&& arg) { // T&& is a universal reference, can match lvalues or rvalues    process(std::forward&lt;T&gt;(arg)); // std::forward converts based on T's type, forwarding as lvalue or rvalue}int main() {    int a = 10;    forwardFunc(a);    // Passing lvalue: T deduced as int&lt;/doubaocanvas&gt;    && , arg is deduced as lvalue reference, std::forward&lt;int&amp;&gt;(arg) forwards as lvalue → calls process (int&)    forwardFunc (20); // Passing rvalue: T deduced as int, arg is deduced as rvalue reference, std::forward&lt;int&gt;(arg) forwards as rvalue → calls process (int&&)    return 0;}
Core Value of Perfect Forwarding: <br />- Ensures that when template functions pass parameters, they do not lose type information, whether it is left value/right value properties or const properties, all can be passed unchanged to the target function; <br />- It is the foundation of "generic programming" in modern C++, widely used in the standard library (such as `std::make_shared`, `std::bind`) and custom template components, reducing code redundancy.

3. Significance Three: Supporting Resource Reuse of “Expiring Values”, Expanding Semantic Expression

In C++11, rvalues are subdivided into “pure rvalues” (such as literal `10`, expression results `a + b`) and “expiring values” (such as lvalues converted to rvalue references by `std::move`, results of functions returning rvalue references). “Expiring values” refer to “objects whose lifecycle is about to end” (such as local variable `a` after being `std::move(a)`, which will not be used afterwards), and rvalue references achieve resource reuse for such objects by binding to “expiring values”, expanding the semantic expression of C++.

#include &lt;string&gt;using namespace std;void printString(string&& s) { // Rvalue reference parameter, can receive expiring values    cout &lt;&lt; s &lt;&lt; endl;}int main() {    string str = "Hello, C++";    // str is an lvalue, converted to expiring value by std::move, passed to printString    printString(std::move(str));     // Note: str has now become an "expiring value", should not be used afterwards (its internal resources may have been transferred)    // cout &lt;&lt; str; // Undefined behavior: str's resources may have been released, content uncontrollable    return 0;}

Core Value:

  • Allows developers to actively “give up” the ownership of resources of lvalues, transferring resources to other objects, achieving “on-demand reuse”;
  • Addresses the traditional C++ limitation of “unable to distinguish between ‘reusable resources’ and ‘non-reusable resources'”, making the code’s semantics clearer (<span>std::move</span> clearly informs the compiler “this object can be moved”).

5. Common Misunderstandings of Lvalue References and Rvalue References

In practical use, developers often confuse the rules of lvalue references and rvalue references. Here are two common misunderstandings and their explanations:

Misunderstanding 1: Believing that rvalue references can only bind to temporary objects and cannot bind to lvalues

Explanation: Rvalue references can only bind to rvalues by default, but through<span>std::move</span>, an lvalue can be converted to an “expiring value” (a type of rvalue), at which point the rvalue reference can bind to that lvalue. However, it should be noted:<span>std::move</span> itself does not move any resources, it only performs a type conversion, and using the lvalue that has been<span>std::move</span> afterwards will lead to undefined behavior.

int a = 10;int&& ref = std::move(a); // Legal: std::move converts a to expiring value, rvalue reference can binda = 20; // Undefined behavior: a's resources may have been bound by ref, modifying a will interfere with ref

Misunderstanding 2: Believing that<span>const T&&</span> (const rvalue reference) has practical uses

Explanation: <span>const T&&</span> is a “const modified rvalue reference”, but the core value of rvalue references is “modifying rvalues (transferring resources)”, while<span>const</span> prohibits modification, leading to<span>const T&&</span> losing the meaning of “move semantics”. In actual development,<span>const T&&</span> is almost useless, only appearing in certain special template matching scenarios, and should be avoided.

const int&& ref = 10;ref = 20; // Error: const rvalue reference prohibits modification of rvalues, losing the value of move semantics

Misunderstanding 3: Believing that returning rvalue references is always more efficient than returning values

Explanation: The premise of returning rvalue references is that “the returned object is an expiring value (such as a local variable)”, but C++’s “return value optimization (RVO/NRVO)” will automatically optimize the copy overhead of return values (constructing objects directly in the caller’s stack frame, no need to copy). If one forcibly returns an rvalue reference (such as returning an rvalue reference of a local variable), it will lead to “dangling references” (the reference points to illegal memory after the local variable is destroyed).

// Error: Returning rvalue reference of a local variable, dangling reference after local variable is destroyedint&& func() {    int a = 10;    return std::move(a); // a is a local variable, destroyed after function ends, returned reference is dangling}// Correct: Directly returning value, compiler will optimize through RVO, no copy overheadint func() {    int a = 10;    return a;}

6. Conclusion: Core Differences Between Lvalue References and Rvalue References and the Value of Rvalue References

1. Summary of Core Differences Between Lvalue References and Rvalue References

Comparison Dimension Lvalue Reference (<span>T&</span>) Rvalue Reference (<span>T&&</span>)
Binding Objects Can only bind to lvalues (addressable persistent objects);<span>const T&</span> can bind to rvalues Can only bind to rvalues (temporary objects, expiring values); must<span>std::move</span> convert lvalues before binding
Lifecycle Impact Does not affect the lifecycle of lvalues (lvalue lifecycle is determined by its own scope) Extends the lifecycle of rvalues (rvalue lifecycle is consistent with the reference)
Core Uses Passing parameters (modifying original objects), returning lvalues (avoiding copies) Implementing move semantics (eliminating copies), perfect forwarding (template parameter passing), reusing expiring value resources
Modification Permissions Can modify bound lvalues (non-const);<span>const T&</span> cannot be modified Can modify bound rvalues (non-const);<span>const T&&</span> has no practical use

2. Core Value of Rvalue References

Rvalue references are not a “replacement” for lvalue references, but rather a “supplement and optimization” for modern C++ semantics, with their core value reflected in three aspects:

  1. Performance Optimization: Eliminating temporary object copy overhead through move semantics, especially for objects containing a large amount of resources (such as<span>std::vector</span>,<span>std::string</span>), significantly improves performance;
  2. Semantic Completeness: Clearly distinguishing between “copy” and “move” semantics through<span>std::move</span> and rvalue references, making code intent clearer, reducing undefined behavior;
  3. Generic Support: Perfect forwarding solves the type loss problem in template parameter passing, serving as the foundation of modern C++ generic programming, supporting many features of the standard library (such as<span>std::tuple</span>,<span>std::function</span>).

Mastering the differences between lvalue references and rvalue references, and understanding the role of rvalue references in move semantics and perfect forwarding, is a key step in learning modern C++ and the foundation for writing efficient and safe code.


Leave a Comment