Lvalue References

In C++, a reference serves as an alias for an existing object. Once defined, operations on the reference actually operate on the referenced object. This allows reading or modifying the referenced object through its alias.

Though references might initially seem unnecessary, they're fundamental to C++ programming. You'll see their importance in upcoming lessons.

Core Understanding

A reference is functionally identical to the object it references.

References can also refer to functions, though this is less common.

Modern C++ has two reference types: lvalue references and rvalue references. This lesson covers lvalue references.

Related Content

Review the previous lesson on value categories (lvalues and rvalues) if needed. Rvalue references are covered in the chapter on move semantics.

Lvalue Reference Types

An lvalue reference (commonly just "reference" pre-C++11) acts as an alias for an existing lvalue, such as a variable.

Reference types determine what they can reference, just as object types determine what values they hold. Lvalue reference types use a single ampersand (&) in the type specifier:

// Regular types
int        // Normal int type (not a reference)
int&       // Lvalue reference to int object
double&    // Lvalue reference to double object
const int& // Lvalue reference to const int object

For example, int& references an int object, while const int& references a const int object.

A type specifying a reference (e.g., int&) is called a reference type. The type being referenced (e.g., int) is the referenced type.

Terminology

Lvalue references have two naming conventions:

  • Lvalue reference to non-const (or just "lvalue reference"): References non-const objects
  • Lvalue reference to const (or "const lvalue reference"): References const objects

This lesson focuses on non-const lvalue references. The next lesson covers const lvalue references.

Lvalue Reference Variables

An lvalue reference variable acts as a reference to an lvalue (typically another variable).

Creating one is straightforward:

#include <iostream>

int main()
{
    int health{100};       // Normal integer variable
    int& alias{health};    // Lvalue reference variable aliasing health

    std::cout << health << '\n';  // Print health's value (100)
    std::cout << alias << '\n';   // Print health's value via alias (100)

    return 0;
}

Here, int& defines alias as an lvalue reference to int, initialized with lvalue health. Thereafter, alias and health are interchangeable, so the program prints:

100
100

The ampersand's position is stylistic. Modern C++ convention places it with the type, clarifying that the reference is part of the type information.

Best Practice

Place the ampersand next to the type name when defining references.

For Advanced Readers

If you're familiar with pointers, note that the ampersand here means "lvalue reference to," not "address of."

Modifying Values Through References

Non-const references allow modifying the referenced object's value:

#include <iostream>

int main()
{
    int health{100};
    int& alias{health}; // alias references health

    std::cout << health << alias << '\n'; // Prints 100100

    health = 75; // health changes to 75

    std::cout << health << alias << '\n'; // Prints 7575

    alias = 50; // Referenced object (health) changes to 50

    std::cout << health << alias << '\n'; // Prints 5050

    return 0;
}

Output:

100100
7575
5050

Since alias references health, changing one changes the other.

Reference Initialization

Like constants, all references must be initialized. This is called reference initialization.

int main()
{
    int& invalid;   // Error: references must be initialized

    int health{100};
    int& alias{health}; // Valid: reference bound to int variable

    return 0;
}

When a reference initializes with an object or function, we say it's bound to that target. This process is reference binding. The referenced object/function is the referent.

Non-const lvalue references can only bind to modifiable lvalues:

int main()
{
    int health{100};
    int& alias{health};       // Valid: bound to modifiable lvalue

    const int maxHealth{100};
    int& invalid{maxHealth};  // Error: can't bind to non-modifiable lvalue
    int& invalid2{0};         // Error: can't bind to rvalue

    return 0;
}

Core Understanding

Allowing non-const lvalue references to bind to const lvalues or rvalues would enable modifying those values through the reference, violating their constness.

Lvalue references to void are prohibited.

References Usually Match Their Referenced Type

Generally, references only bind to objects matching their referenced type (with inheritance exceptions discussed later).

If you attempt binding a reference to a mismatched type, the compiler tries implicit conversion, creating an rvalue. Since non-const lvalue references can't bind to rvalues, this fails:

Core Understanding

Conversion results create rvalues, and non-const lvalue references can't bind to rvalues. Therefore, binding a non-const lvalue reference to a mismatched type causes compilation errors.

int main()
{
    int health{100};
    int& alias{health};         // Valid: types match

    double rating{4.5};
    int& invalid{rating};       // Error: narrowing conversion disallowed
    double& invalid2{health};   // Error: can't bind to rvalue (converted health)

    return 0;
}

References Cannot Be Reseated

Once initialized, a reference cannot be reseated (changed to reference a different object).

Beginners often try reseating references via assignment, which compiles but doesn't work as expected:

#include <iostream>

int main()
{
    int health{100};
    int mana{50};

    int& alias{health}; // alias references health

    alias = mana; // Assigns 50 (mana's value) to health
    // Does NOT make alias reference mana!

    std::cout << health << '\n'; // User expects 100

    return 0;
}

Surprisingly, this prints:

50

When alias = mana executes, alias resolves to its referent (health). So the statement becomes health = mana, assigning mana's value (50) to health. The reference alias still references health.

Reference Scope and Duration

References follow the same scoping and duration rules as normal variables:

#include <iostream>

int main()
{
    int health{100};
    int& alias{health};

     return 0;
} // health and alias destroyed here

Independent Lifetimes

With one exception (covered next lesson), references and referents have independent lifetimes:

  • References can be destroyed before their referents
  • Referents can be destroyed before their references

When a reference dies first, the referent is unaffected:

#include <iostream>

int main()
{
    int health{100};

    {
        int& alias{health};
        std::cout << alias << '\n'; // Prints 100
    } // alias destroyed here, health unaffected

    std::cout << health << '\n'; // Prints 100

    return 0;
} // health destroyed here

Output:

100
100

When alias is destroyed, health continues normally, unaware of the reference's destruction.

Dangling References

When a referent is destroyed before its reference, the reference becomes dangling. Accessing dangling references causes undefined behavior.

Dangling references are usually avoidable. We'll show a practical example in a future lesson on returning by reference.

References Aren't Objects

Surprisingly, references aren't objects in C++. No storage requirement exists for references. When possible, compilers optimize references away, replacing them with their referents. However, this isn't always possible, so references may sometimes require storage.

This makes "reference variable" somewhat misleading, since variables are named objects, and references aren't objects.

Because references aren't objects, they can't appear where objects are required (like references to references, since lvalue references must reference identifiable objects). For reseatable references or reference objects, use std::reference_wrapper (covered in the lesson on aggregation).

As an Aside

Consider:

int data{};
int& ref1{data};  // Lvalue reference bound to data
int& ref2{ref1};  // Lvalue reference bound to data

You might think ref2 is a reference to ref1 (a reference to a reference), but it's not. Since ref1 is a reference to data, when used in an expression (like an initializer), ref1 evaluates to data. So ref2 is a normal lvalue reference (type int&) bound to data.

A reference to a reference (to an int) would have syntax int&&, but C++ doesn't support references to references. This syntax was repurposed in C++11 for rvalue references (covered in the move semantics chapter).

For Context
References may seem pointless now, but they're extensively used. We'll explore their primary use cases in upcoming lessons on pass by lvalue reference and pass by const lvalue reference.

Quiz

Question 1

Determine this program's output without running it:

#include <iostream>

int main()
{
    int gold{10};
    int& pouch{gold};

    std::cout << gold << pouch << '\n';

    int silver{20};
    pouch = silver;
    silver = 30;

    std::cout << gold << pouch << '\n';

    gold = 40;

    std::cout << gold << pouch << '\n';

    return 0;
}

Answer:

1010
2020
4040

Since pouch is bound to gold, they're synonymous and always print the same value. The line pouch = silver assigns silver's value (20) to pouch, not rebinding pouch to silver. Subsequently changing silver to 30 only affects silver.

Summary

References: Aliases for existing objects. Once defined, operations on the reference actually operate on the referenced object. Functionally identical to the object they reference.

Lvalue references: Reference types using & in type declaration (e.g., int&). Create aliases for lvalues (typically variables). The modern C++ category (pre-C++11, just called "references").

Reference initialization (binding): All references must be initialized. When initialized with an object, the reference is "bound" to that object (the "referent"). Non-const lvalue references can only bind to modifiable lvalues.

Type matching: References generally only bind to objects of matching type. Attempting to bind to mismatched types causes implicit conversion creating an rvalue, which non-const lvalue references cannot bind to.

Cannot be reseated: Once bound, references cannot be changed to reference different objects. Assignment through a reference assigns to the referent, not rebinding the reference.

Scope and duration: References follow same scoping/duration rules as variables. References and referents have independent lifetimes (with one exception covered in next lesson).

Dangling references: References become dangling when their referent is destroyed before the reference. Accessing dangling references causes undefined behavior.

Not objects: References aren't objects in C++. No storage requirement. Often optimized away by compiler. Cannot have references to references or arrays of references.

Ampersand placement: Modern C++ convention places & next to type name (int& ref) to show it's part of the type information.

Primary uses: Extensively used for passing parameters efficiently and safely (covered in upcoming lessons on pass by reference).

Understanding lvalue references is fundamental to modern C++ programming, enabling efficient function parameter passing, avoiding copies, and providing alternative names for objects.