Skip to content

References — Lvalue References, Rvalue References, Reference vs Pointer

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about References. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ references provide aliases to objects with lvalue references binding to persistent objects and rvalue references binding to temporaries, supporting move semantics and perfect forwarding.

What You'll Learn

You will declare and use lvalue references (T&) and rvalue references (T&&), understand when to use references versus pointers, apply reference parameters for efficient function calls, use const references to extend temporary lifetimes, and understand reference collapsing and forwarding references (T&& in templates).

Why It Matters

References are fundamental to C++. They enable pass-by-reference without pointer syntax, form the basis of move semantics (rvalue references), and are essential for operator overloading (especially stream operators). The C++ standard library relies heavily on reference semantics for efficiency. Understanding references deeply separates intermediate from advanced C++ programmers.

Learning Path

graph LR
    A["21: Pointers"] --> B["22: References"]
    B --> C["23: Dynamic Memory"]
    C --> D["24: Smart Pointers"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Lvalue References

#include <iostream>

int main() {
    int x = 42;
    
    // Reference declaration: alias for x
    int& ref = x;
    
    std::cout << x << "\n";    // 42
    std::cout << ref << "\n";  // 42
    
    ref = 99;  // modifies x
    std::cout << x << "\n";    // 99
    
    // References must be initialized
    // int& bad;  // Error: reference must be initialized
    
    // Cannot change what a reference refers to
    int y = 10;
    ref = y;  // This does NOT make ref refer to y; it assigns y's value to x
    std::cout << x << "\n";    // 10 (x's value changed)
    std::cout << &x << "\n";   // same as &ref (unchanged)
    
    // References to const extend temporary lifetimes
    const int& tempRef = 42;  // OK: temporary bound to const ref
    std::cout << tempRef << "\n";  // 42
}

Key properties of references:

  • Must be initialized when declared
  • Cannot be made to refer to a different object after initialization
  • Cannot be null (they must refer to a valid object)
  • Syntax is the same as value access (no explicit dereferencing)

References as Function Parameters

#include <iostream>
#include <string>

// Pass by reference: no copy, can modify
void toUpper(std::string& s) {
    for (char& c : s) {
        c = std::toupper(static_cast<unsigned char>(c));
    }
}

// Pass by const reference: no copy, read-only
void print(const std::string& s) {
    std::cout << s << "\n";
}

int main() {
    std::string msg = "hello world";
    print(msg);      // no copy
    toUpper(msg);    // no copy, modified in place
    print(msg);      // HELLO WORLD
}

References as Return Types

#include <iostream>
#include <vector>

class Array {
private:
    int data_[5] = {1, 2, 3, 4, 5};
    
public:
    // Return reference to allow assignment: arr[i] = value
    int& operator[](size_t index) {
        return data_[index];
    }
    
    // Return const reference for read-only access
    const int& operator[](size_t index) const {
        return data_[index];
    }
};

int main() {
    Array arr;
    arr[2] = 99;  // works because operator[] returns reference
    std::cout << arr[2] << "\n";  // 99
    
    // Never return a reference to a local variable
    // int& bad() { int x = 5; return x; }  // dangling reference
}

Rvalue References (T&&)

#include <iostream>
#include <string>
#include <vector>

void process(const std::string& s) {
    std::cout << "lvalue: " << s << "\n";
}

void process(std::string&& s) {
    std::cout << "rvalue: " << s << "\n";
}

int main() {
    std::string s = "hello";
    
    process(s);           // calls lvalue overload (const&)
    process("world");     // calls rvalue overload (&&)
    process(std::move(s)); // calls rvalue overload (&&)
    
    // Rvalue references are used in move constructors
    // std::vector<int> v1 = {1,2,3};
    // std::vector<int> v2 = std::move(v1);  // move constructor
}

Rvalue references bind to temporaries. They enable move semantics by allowing functions to "steal" resources from objects that are about to be destroyed.

Reference Collapsing and Forwarding References

When T&& is used in a template context (not with a concrete type), it becomes a forwarding reference (also called universal reference).

#include <iostream>
#include <string>
#include <utility>

// T&& is a forwarding reference here
template <typename T>
void forwarder(T&& arg) {
    // Reference collapsing rules:
    // T& & -> T&
    // T& && -> T&
    // T&& & -> T&
    // T&& && -> T&&
    process(std::forward<T>(arg));
}

void process(int& x) {
    std::cout << "lvalue: " << x << "\n";
}

void process(int&& x) {
    std::cout << "rvalue: " << x << "\n";
}

int main() {
    int x = 5;
    forwarder(x);     // calls lvalue process
    forwarder(10);    // calls rvalue process
}

std::forward preserves the value category of the argument. This is perfect forwarding: the template passes arguments exactly as they were received.

Reference vs Pointer Cheat Sheet

Feature Reference Pointer
Must be initialized Yes No (but should be)
Can be null No Yes
Can be reassigned No Yes
Dereference syntax Implicit (ref) Explicit (*ptr)
Address syntax &ref (gives address) ptr (gives address)
Array support No Yes (array decay)
Rebind to other object No Yes
Use with STL containers Yes (with caution) Yes
Reassignment ref = val (changes value) ptr = &val (changes pointer)

Rvalue Reference Lifetime Extension

#include <iostream>
#include <string>

std::string createGreeting(const std::string& name) {
    return "Hello, " + name + "!";
}

int main() {
    // const reference extends the lifetime of the temporary
    const std::string& ref = createGreeting("Alice");
    std::cout << ref << "\n";  // OK: temporary still alive
    
    // Rvalue reference also extends lifetime
    std::string&& rref = createGreeting("Bob");
    std::cout << rref << "\n";  // OK
    
    // But this does not apply to function return values by default
}

Common Mistakes

Mistake 1: Reference to Local Variable

int& getValue() {
    int x = 5;
    return x;  // x destroyed, dangling reference
}

Never return a reference to a stack-local variable.

Mistake 2: Uninitialized Reference

int& ref;  // Error: must be initialized
int* ptr;  // OK (but bad practice)

Mistake 3: Confusing Reference Assignment with Rebind

int a = 1, b = 2;
int& ref = a;
ref = b;  // Does NOT make ref refer to b; assigns b's value to a

Mistake 4: Non-const Reference to Temporary

void increment(int& x) { ++x; }
// increment(5);  // Error: cannot bind non-const reference to temporary

Use const int& for read-only, or accept by value.

Mistake 5: Storing References in Containers

std::vector<int&> vec;  // Error: cannot have container of references

Use std::reference_wrapper or pointers instead.

Mistake 6: Using std::move to Avoid a Copy Unnecessarily

std::string s = "hello";
std::string t = std::move(s);  // moves, but s is now empty
// If you still need s, do not move it

Practice Questions

  1. What is the difference between int& and int&&?
  2. Why must references be initialized when declared?
  3. Write a function that swaps two integers using references.
  4. What problem does std::forward solve?
  5. Can you have a reference to a reference? Explain reference collapsing.

Challenge

Implement a move_if_noexcept function that uses std::is_nothrow_move_constructible and returns an rvalue reference (for moving) or const lvalue reference (for copying) based on whether the type has a noexcept move constructor.

FAQ

Can a reference be null?

No. A reference must be initialized with a valid object. However, you can create a reference from a dereferenced null pointer (int& ref = *nullptr;), which is undefined behavior.

What is the size of a reference?

References themselves do not have a size in the C++ abstract machine. The compiler typically implements them as pointers internally, so they consume pointer-sized storage when not optimized away.

Why does C++ have both references and pointers?

References provide a safer, more convenient syntax for aliasing. Pointers are more flexible (reassignable, nullable) and are needed for dynamic memory, iterators, and C interop.

What is perfect forwarding?

A template pattern using T&& and std::forward that passes arguments to another function while preserving their value category (lvalue vs rvalue).

Can I have a reference to void?

No. void& is not valid because void is an incomplete type.

What is a 'dangling reference'?

A reference that refers to memory that has been freed or to an object that has been destroyed. Using it is undefined behavior.

Mini Project

Build a simple reference-counted string handler:

#include <iostream>
#include <cstring>

class SharedString {
private:
    struct ControlBlock {
        char* data;
        int refCount;
    };
    
    ControlBlock* block_;
    
public:
    SharedString(const char* str = "") {
        block_ = new ControlBlock;
        block_->data = new char[std::strlen(str) + 1];
        std::strcpy(block_->data, str);
        block_->refCount = 1;
    }
    
    // Copy: increment reference count
    SharedString(const SharedString& other) : block_(other.block_) {
        ++block_->refCount;
        std::cout << "Shared: ref count = " << block_->refCount << "\n";
    }
    
    // Move: transfer ownership, no reference counting
    SharedString(SharedString&& other) noexcept
        : block_(other.block_) {
        other.block_ = nullptr;
    }
    
    ~SharedString() {
        if (block_ && --block_->refCount == 0) {
            delete[] block_->data;
            delete block_;
            std::cout << "Resources freed\n";
        }
    }
    
    const char* c_str() const { return block_ ? block_->data : ""; }
    
    // Return by value uses copy semantics
    SharedString toUpper() const {
        SharedString result(block_->data);
        for (char* p = result.block_->data; *p; ++p) {
            *p = std::toupper(static_cast<unsigned char>(*p));
        }
        return result;
    }
};

int main() {
    SharedString s1("Hello");
    SharedString s2 = s1;
    SharedString s3 = std::move(s1);
    SharedString s4 = s2.toUpper();
    std::cout << s2.c_str() << " " << s3.c_str() << " " << s4.c_str() << "\n";
}

What's Next

References are essential for efficient parameter passing. The next lesson covers dynamic memory allocation: new, delete, new[], delete[], and how to avoid memory leaks.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro