Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Creating References to Objects
Create aliases to existing objects that cannot be reseated to refer elsewhere.
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).
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.
Creating References to Objects - Quiz
Test your understanding of the lesson.
Practice Exercises
Lvalue References
Practice creating and using lvalue references as aliases to existing variables. Understand how references differ from pointers.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!