References
Another name for an existing object, and how that differs from a pointer.
By the end of this chapter you can
- Choose between a reference and a pointer for a given interface
- Explain why a reference cannot be reseated
- Identify when a reference outlives what it refers to
A reference is a second name for an object that already exists. Not a copy, not a pointer to it — a name. Once bound, it refers to that object for the rest of its life, and every operation on the reference is an operation on the original.
This is the tool you reach for far more often than pointers. Most C++ code that needs to talk about an existing object uses a reference; pointers come out when “no object” has to be representable, or when the thing being referred to has to change.
Binding a reference
#include <iostream>
int main() {
int original = 10;
int& alias = original; // alias IS original, under another name
alias = 25;
std::cout << "original = " << original << '\n';
original = 7;
std::cout << "alias = " << alias << '\n';
std::cout << "same address? "
<< (&original == &alias ? "yes" : "no") << '\n';
}&original == &alias because there is one object. Taking the address of a
reference gives you the address of what it refers to — there is no separate
“reference object” to point at.
Compare that with the pointer version from the last chapter: a pointer is its own object with its own address, holding a number. A reference adds no storage you can observe.
A reference must be initialised, and cannot be moved
Two rules follow from “a reference is a name”:
int main() {
int& dangling; // a name for what?
}There is no such thing as an unbound reference. The compiler rejects it outright — compare that with a pointer, which is perfectly happy to be uninitialised and then explode later.
The second rule is subtler:
#include <iostream>
int main() {
int a = 1, b = 2;
int& ref = a;
ref = b; // NOT "make ref refer to b" — this writes b's value into a
std::cout << "a = " << a << ", b = " << b << '\n';
std::cout << "ref names a? " << (&ref == &a ? "yes" : "no") << '\n';
}ref = b copied b’s value into a. It did not make ref name b. Once
bound, a reference cannot be reseated, and there is no syntax that would do
it: every use of the name means the object.
What references are actually for
Passing without copying
#include <iostream>
#include <string>
void by_value(std::string s) { s += " (changed)"; }
void by_reference(std::string& s) { s += " (changed)"; }
void by_const_ref(const std::string& s) { std::cout << "read: " << s << '\n'; }
int main() {
std::string text = "hello";
by_value(text);
std::cout << "after by_value: " << text << '\n';
by_reference(text);
std::cout << "after by_reference: " << text << '\n';
by_const_ref(text);
}by_value copied the whole string — allocation and all — modified the copy, and
threw it away. by_reference operated on the caller’s object.
The default you want for anything bigger than a pointer is const T&: no copy,
and a compiler-enforced promise not to modify. It is so common that reading
const std::string& should feel like reading a single word.
The cost is real and measurable:
#include <chrono>
#include <iostream>
#include <string>
std::size_t by_value(std::string s) { return s.size(); }
std::size_t by_const_ref(const std::string& s) { return s.size(); }
int main() {
const std::string big(100'000, 'x');
constexpr int rounds = 2000;
auto time = [&](auto&& fn) {
auto start = std::chrono::steady_clock::now();
std::size_t total = 0;
for (int i = 0; i < rounds; ++i) total += fn(big);
auto finish = std::chrono::steady_clock::now();
std::cout << " (checksum " << total << ") ";
return std::chrono::duration_cast<std::chrono::microseconds>(finish - start).count();
};
std::cout << "by value: " << time(by_value) << " us\n";
std::cout << "by const ref: " << time(by_const_ref) << " us\n";
}Same answer, and the by-value version does a hundred thousand bytes of copying two thousand times to get it.
Returning something the caller can modify
#include <iostream>
#include <vector>
int& largest(std::vector<int>& v) {
int* best = &v[0];
for (int& x : v) {
if (x > *best) best = &x;
}
return *best;
}
int main() {
std::vector<int> data{3, 9, 4};
largest(data) = 0; // assign through the returned reference
for (int x : data) std::cout << x << ' ';
std::cout << '\n';
}largest(data) = 0 looks strange the first time. It works because largest
returns a name for an element that still exists, so assigning to that name
assigns to the element. This is exactly how v[i] and m[key] work.
Range-based for, without copies
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> words{"alpha", "beta", "gamma"};
for (std::string w : words) w += "!"; // modifies copies
std::cout << "after by-value loop: " << words[0] << '\n';
for (std::string& w : words) w += "!"; // modifies the elements
std::cout << "after by-reference: " << words[0] << '\n';
for (const std::string& w : words) std::cout << w << ' ';
std::cout << '\n';
}The first loop is a common bug and a common waste: it copies every element,
modifies the copy, and discards it. for (const auto& x : container) should be
your reflex.
References can dangle too
A reference is safer than a pointer in that it is never null and never uninitialised. It is not safer about lifetime:
#include <string>
const std::string& make_greeting() {
std::string greeting = "hello";
return greeting; // greeting dies at the closing brace
}
int main() {
const std::string& r = make_greeting();
return static_cast<int>(r.size());
}The compiler warns — -Wreturn-local-addr again — and, exactly as with the
pointer version, hands back a null reference rather than one into dead stack, so
the program fails immediately with reference binding to null pointer. And
exactly as with pointers, that help stops the moment the mistake spans two
functions, where no warning is possible and only the sanitizer notices.
A reference is therefore not a lifetime guarantee. It guarantees that a valid object existed when the reference was bound; keeping it valid afterwards is still your job.
The reference-specific version of the trap involves temporaries:
#include <iostream>
#include <string>
std::string build() { return "a temporary string"; }
int main() {
// Binding a temporary to a const reference extends its lifetime
// to match the reference. This is safe, and deliberate.
const std::string& kept = build();
std::cout << kept << '\n';
// But lifetime extension does NOT pass through a function return,
// and it does not apply to a reference to a member of a temporary.
std::cout << "still alive: " << kept.size() << " characters\n";
}Reference or pointer?
| Use a reference when | Use a pointer when |
|---|---|
| The object definitely exists | “No object” is a valid state (use nullptr) |
| You will refer to the same object throughout | The target must change over time |
| You want the call site to look like a normal value | You want the call site to show that something may be modified, via &x |
| Passing a parameter you read or modify | Iterating raw memory, or interfacing with C |
When both would work, prefer the reference. It cannot be null, cannot be
uninitialised, and needs no * at every use.