References — Lvalue References, Rvalue References, Reference vs Pointer
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
- What is the difference between
int&andint&&? - Why must references be initialized when declared?
- Write a function that swaps two integers using references.
- What problem does
std::forwardsolve? - 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
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