Understanding Left Values, Right Values, and Rvalue References in C++

A left value (lvalue) refers to an expression that can have its address taken using the & operator. These expressions typically appear on the left side of an assignment statement and represent identifiable objects in memory.

Examples of left values:

int i = 42; // i is an lvalue
int *p = &i; // i is an lvalue, address can be retrieved via &
int& demoFunction() {
    return i;
}

demoFunction() = 42; // demoFunction returns an lvalue reference

int *p1 = &demoFunction(); // Valid usage of lvalue reference

Lvalues generally denote an object's identity and memory location. They are usually modifiable, although const lvalues should not be altered.

Characteristics of lvalues:

  • Addressability: Lvalues can be addressed with the & operator.
  • Assignability: They can appear on the left side of an assignment.

Types of lvalues include:

  • Variable names (e.g., int x;)
  • Array elements (e.g., arr[0])
  • Member access (e.g., obj.member)
  • Dereferenced pointers (e.g., *ptr)

Right Values

In C++, a right value (rvalue) represents a temporary object or literal that does not have a persistent memory address. These typically appear on the right side of an assignment and are ephemeral.

Rvalues often involve literals, temporary objects, or results of expressions:

// Literals are rvalues
int a = 10;
char b = 'A';

// Function return value is an rvalue
int c = generateResult(20, 10);

// Expression result is an rvalue
int m = a + b;

// String literal is an rvalue
const char *pName = "hello world";

// nullptr is an rvalue
int32_t *p = nullptr;

DemoClass obj = DemoClass();

Not all function return values are rvalues; they may also be lvalues if a reference is returned:

int& testLValueFunction() {
    int i;
    return i;
}

int testRValueFunction() {
    int i = 5;
    return i;
}

{
  testLValueFunction() = 10; // Valid - returns lvalue reference
  int *p1 = &testLValueFunction();
}

{
  // testRValueFunction() = 10; // Invalid - returns rvalue
}

Left and Right Value References

In C++, references act as aliases for variables. A left value reference binds to lvalues, denoted by &. A right value reference binds to rvalues, denoted by &&.

C++11 introduced rvalue references to support move semantics and perfect forwarding.

class DemoClass {...};

// Accepts lvalue reference
void foo(X& x);

// Accepts rvalue reference
void foo(X&& x);

X x;
foo(x); // Calls foo(X&)

X bar();
foo(bar()); // Calls foo(X&&)

Overloading functions for both lvalue and rvalue references allows distinct behavior for each type:

void foo(const X& x); // Accepts both lvalue and rvalue

void foo(X&& x); // Only accepts rvalue

X x;
foo(x); // Invokes const X& version

X bar();
foo(bar()); // Invokes X&& version

Declaring rvalue references:

int a = 10;
int &lvalue_ref = a; // lvalue reference

int &&rvalue_ref = 10 + 20; // rvalue reference

Rvalue References and Move Constructors

Consider a container class holding a pointer to a large object. Copying this object incurs high overhead. Implementing a move consturctor avoids unnecessary copying:

class BasicClass {
public:
    BasicClass() { std::cout << "construct\n"; }
    ~BasicClass() = default;
    BasicClass(const BasicClass& ref) { 
        std::cout << "copy construct\n"; 
    }
};

class ContainerClass {
private:
    BasicClass *p = nullptr;
public:
    ContainerClass() { p = new BasicClass(); }
    ~ContainerClass() { delete p; }

    ContainerClass(const ContainerClass& ref) {
        std::cout << "copy construct\n";
        p = ref.p;
    }

    ContainerClass& operator=(const ContainerClass& ref) {
        std::cout << "assignment\n";
        BasicClass* tmp = new BasicClass(*ref.p);
        delete this->p;
        this->p = tmp;
        return *this;
    }

    // Move assignment operator
    ContainerClass& operator=(ContainerClass&& rhs) noexcept {
        std::cout << "move assignment\n";
        std::swap(this->p, rhs.p);
        return *this;
    }
};

When assigning a temporary object, the move assignment is invoked instead of the copy assignment:

static ContainerClass createObject() {
    return ContainerClass();
}

{
    ContainerClass p;
    p = createObject(); // Uses move assignment
}

std::move and Move Semantics

The std::move function converts an lvalue into an rvalue, enabling move operations:

{
    ContainerClass p;
    ContainerClass q;
    p = std::move(q); // Move q into p
}

{
    ContainerClass p;
    p = std::move(createObject()); // Move temporary into p
}

Universal References

Universal references enable perfect forwarding in templates:

template<typename T>
void foo(T&& param);

int x = 27;
const int cx = x;
const int& rx = cx;

foo(x);     // T is int&, param is int&
foo(cx);    // T is const int&, param is const int&
foo(rx);    // T is const int&, param is const int&
foo(27);    // T is int, param is int&&

std::forward for Perfect Forwarding

std::forward preserves the value category of forwarded arguments:

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v4(Arg&& arg) { 
    return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

// Equivalent to:
// shared_ptr<a> factory_v4(int& arg) { return shared_ptr</a><a>(new A(std::forward<int>(arg))); }
// shared_ptr<a> factory_v4(int&& arg) { return shared_ptr</a><a>(new A(std::forward<int>(arg))); }
</int></a></int></a>

Return Value Optimization

Modern compilers perform Return Value Optimization (RVO), wich avoids unnecessary copies:

BasicClass foo() {
    BasicClass x;
    return x; // No copy due to RVO
}

BasicClass bar() {
    BasicClass x;
    return std::move(x); // Still optimized due to RVO
}

Safe Move Operations

When containers like std::vector reallocate memory, they prefer move constructors if available:

  • If a move constructor is noexcept, it's used for efficient transfer.
  • Otherwise, the copy constructor is used.

Tags: C++ left-value right-value rvalue-reference memory-management

Posted on Sat, 18 Jul 2026 16:44:10 +0000 by mithras