Lesson 06 / 14
References
Measures that a reference parameter works with the caller's own object, producing no copy at all.
Published Updated
In this lesson
- Measures that a reference parameter works with the caller's own object
- Applies the choice among value, reference, and const reference to when each one is used
- Distinguishes that a const reference can bind to a temporary object, while an ordinary reference cannot
Contents
The previous lesson measured that an object takes its own memory when it is copied. This
lesson measures the exact opposite question: if a function takes an object by
reference, is any copy produced at all, or does the function work directly with the
caller’s own object? The measurement kind is self: when p is given to the function
nudge(Point& n), the n inside the function and the p in the caller are the same
object — &p == &n is true, and no copying step happens between them.
In C, the only way to pass a struct to a function “by reference” was to pass a pointer:
nudge(&p), accessed inside the function with (*ptr).x or ptr->x. This forced the
& operator to be written by hand at the call site, and forced a dereference (*) or
the arrow operator to be written at every access inside the function — and there was
always a possibility that ptr was nullptr. In C++ a reference removes both problems
at once: nudge(p) is written at the call site, no & is needed; n.x is written
inside the function, no dereference is needed; and because a reference cannot be left
uninitialized, the nullptr case is ruled out by the language itself.
What a Reference Is
A reference is a second name given to an existing object — it is not a new object,
and it does not get a new memory address. When Point& r = p; is written, r is p
itself; every read and write made through r reaches p’s memory directly. This is
different from a pointer (Point* ptr = &p;): a pointer is a separate object, has its
own memory, and holds p‘s address; a reference, on the other hand, is the same
thing as p itself, and occupies no separate memory location (the compiler often turns
a reference into a pointer in the generated code, but in the language’s own model a
reference is not a new object).
The practical consequence of this distinction is this: a pointer does not guarantee,
before ptr->x is written, that it points at a valid object — it may need to be checked
by hand (if (ptr != nullptr)). There is no such check for a reference, and none is
needed — the language assumes that a reference is always bound to a valid object (code
that deliberately breaks this assumption, for example by dereferencing an invalid
pointer and binding it to a reference, produces undefined behavior, but no code within
this lesson’s scope does anything like that). This is why a function signature that
takes a reference requires less defensive code than one that takes a pointer — the
existence of the parameter is accepted from the start.
// point.hpp — a simple struct for measuring passage by reference
#ifndef POINT_HPP
#define POINT_HPP
struct Point {
int x;
int y;
};
#endif
A Struct Passed by Reference
// reference-measurement.cpp — does nudge(Point&) work with the caller's own object
#include <cstdio>
#include "point.hpp"
void nudge(Point& n, Point* caller_address) {
std::printf("is n inside nudge the same object as the caller %s\n",
&n == caller_address ? "yes" : "no");
n.x += 10;
n.y += 10;
}
int main() {
Point p{1, 2};
nudge(p, &p);
std::printf("after nudge p.x=%d p.y=%d\n", p.x, p.y);
return 0;
}
is n inside nudge the same object as the caller yes after nudge p.x=11 p.y=12
CR12. nudge(Point& n, Point* caller_address) takes its argument by reference; when
nudge(p, &p) is called, n is p itself — &n == caller_address (that is,
&n == &p) is true. Although the line n.x += 10; writes through n, because n and
p are the same object this write directly changes p.x; after nudge returns, p.x
and p.y are already updated (11 and 12) — main continues with the caller’s
object changed, without receiving any return value and without performing any
assignment. This is the exact opposite of the previous lesson’s copy kind: there,
b was a different address from a; here, n is the same address as p — no
copying step runs at all, because passing a reference argument never calls the copy
constructor.
Value, Reference, const Reference: Which One, When
A function has three options when it takes an object as an argument, and each one encodes a different intent:
- By value (
Point n): the function works with its own copy, and never affects the caller’s object in any way. This suits small, cheaply copied types (anint, aPointof twoints); for large or expensively copied types (a class managing its own memory, likeBufferin the third lesson), it produces a needless copy on every call. - By reference (
Point& n): the function works directly with the caller’s object, and can modify it.nudgedoes this — its signature tells the reader “this function can modify the object you give it.” - By const reference (
const Point& n): the function accesses the caller’s object without copying it, but cannot modify it —n.x += 10;would be a compile error here. This encodes the intent “read a large object cheaply, but do not modify it”; this is exactly why the copy constructor’s and copy assignment’s argument in the previous lessons wasconst Buffer&.
The intent is understood by reading the signature: void write(const Point& n) only
reads the caller’s object; void nudge(Point& n) modifies it; void copy(Point n)
never touches it at all, and works on its own copy.
There is no shortcut among these three options like “always use a reference, it is
faster.” Taking a small type that fits in a single machine word, like int, by
reference generally gains nothing — passing a reference also generally means (depending
on the code the compiler produces) passing an address, and an address is typically the
same size as an int; moreover, because a function taking a reference can now modify
the caller’s object, even a read-only use is left open to an accidental write (this risk
is closed by const int&, but then no gain remains either). Taking small types by value
is both simpler and, in most cases, equally fast. The real gain from reference and const
reference appears for types that are expensive to copy — classes that allocate their own
memory, like Buffer in the third and fifth lessons, or large structs with many
members.
Reference Parameters and Return Values
A reference can be used not only for parameters but also for return types. A function
like Point& first(Point array[]) can return the array’s first element as itself,
not as a copy — the caller can write first(d) = Point{0, 0}; and directly change the
array’s first element, because the return value is d[0] itself. This lesson does not
run a reference return value (this is the typical usage of overloaded operators such as
operator[], and is covered in K05), but it rests on the same principle as parameter
references: returning a reference is granting access to an existing object without
constructing a new one.
Reference Arguments in Member Functions
This principle works the same way for member functions too. The fifth lesson’s
Buffer::operator= signature was Buffer& operator=(const Buffer& other) — the other
parameter here, too, is the caller’s own object; when b = a; is written, other
inside operator= is the same address as a. Every line where operator= reads
other.data_ reads a’s memory directly; if other had been taken by value (as
Buffer other), every b = a; call would first build a Buffer copy, and operator=
would work with that copy — meaning both a needless copy constructor call and a
needless destructor call. The choice of const Buffer& carries the same intent here
too: read a without copying it, do not modify it.
A Reference Cannot Be Rebound
Once a pointer is set, it can be changed to point at a different object:
ptr = &another;. This is not possible for a reference — after Point& r = p; is
written, r = another; is not a rebinding; it assigns another‘s value onto r
(that is, onto p). Which object a reference is bound to is fixed the moment the
reference is created, and this binding never changes for the reference’s lifetime —
this is why a reference must be initialized at the moment it is created; there is
no such thing as an empty reference (unlike a pointer’s nullptr).
Binding a const Reference to a Temporary
// temporary-binding-measurement.cpp — can a const reference bind to a temporary
#include <cstdio>
#include "point.hpp"
void write(const Point& n) {
std::printf("inside write x=%d y=%d\n", n.x, n.y);
}
int main() {
Point p{1, 2};
write(p);
const Point& r = Point{3, 4};
std::printf("via the temporary x=%d y=%d\n", r.x, r.y);
write(Point{5, 6});
return 0;
}
inside write x=1 y=2 via the temporary x=3 y=4 inside write x=5 y=6
CR13. The line const Point& r = Point{3, 4}; binds a const reference to a
temporary object (Point{3, 4}, an unnamed value constructed in the middle of the
expression). Normally a temporary’s lifetime ends at the end of the expression that
constructs it, but when it is bound to a const reference the C++ standard extends
that temporary’s lifetime to match the reference’s lifetime — as long as r is valid,
Point{3, 4} is also valid, so reading r.x and r.y is not undefined behavior. If
the same binding had been written as Point& r = Point{3, 4}; (without const), it
would not compile: an ordinary reference cannot bind to a temporary, because allowing a
write through the temporary (like r.x = 99;) would mean modifying an object the
caller could never see, one that is about to disappear immediately — the language rules
this out from the start as meaningless. The call write(Point{5, 6}) uses the same
rule: because write’s parameter is const Point&, the temporary can bind directly;
if the parameter were an ordinary Point&, this call would not compile.
References and Memory Layout
M11/K01 covered a struct’s memory layout — member order, alignment, and padding. A
reference does not establish a “new memory layout” in that lesson’s sense — when
Point& r = p; is written, r has no sizeof value or memory layout of its own,
because r is not a new object, it is another name for p. A class with a reference
member (a member like Point& reference_;) may look like an exception to this rule —
such a member typically occupies the space of a pointer, because the compiler turns it
into code that way — but this is an implementation detail; the language itself does not
define “where” a reference is kept, only which object it is bound to.
The Bounding Measurement
The expression this lesson measures is of kind self — but only in some cases. A
parameter taking const Point& can be either self or temporary; the signature
itself does not say which one, only the argument at the call site determines it.
// self-or-temporary.cpp — which kind does a const reference parameter fall under, in each case
#include <cstdio>
#include "point.hpp"
void write(const Point& n) {
std::printf("inside write x=%d y=%d\n", n.x, n.y);
}
int main() {
Point p{7, 8};
write(p);
write(Point{9, 10});
std::printf("did p stay unchanged %s\n", (p.x == 7 && p.y == 8) ? "yes" : "no");
return 0;
}
inside write x=7 y=8 inside write x=9 y=10 did p stay unchanged yes
CR14. In the call write(p), n is p itself — kind self; in the call
write(Point{9, 10}), n is bound to an unnamed temporary constructed in the middle
of the expression — kind temporary. Both calls use the same signature
(const Point&) and call the same function, but which kind the parameter falls under
is determined only by the argument itself at the call site. Being const prevents the
function from writing through n in both cases — this is why p remains unchanged on
the third line; whether write’s n is self or temporary has no effect on p’s state
at all. It is not possible to give a definite answer to “is this parameter self” by
looking at a function signature alone — the signature only answers the question can
it be modified (reference or const reference); the answer to whether it is self or
temporary is determined anew at each call.
This limit produces a consequence worth considering when designing a class: seeing that
a function accepts a const Point& parameter does not guarantee that the function will
always be called with a persistent object. The function body must assume that the
reference it receives may have a lifetime that ends at the close of the call
expression — for example, storing n‘s address somewhere and using it after the
function returns (something like Point* p = &n; return p;) produces a dangling
pointer if n is bound to a temporary. This lesson does not run such code; it only
states the limit. The reason the next lesson’s operator+ returns a new object
rather than a reference comes from the same limit: the sum the operator produces is a
local value constructed inside the operator’s own body, and if it needs to live on
after the function returns, it must be returned by value, not by reference.
Summary
- A reference is a second name given to an existing object; it does not construct a new object, and does not take a new address.
- An argument taken by reference is the caller’s self — no copying step runs,
&n == &pis true. - The choice among value, reference, and const reference encodes intent: copy-and-do-not-touch, modify, or read-without-copying-but-do-not-modify.
- Once a reference is created, it cannot be rebound to a different object.
- A
constreference can bind to a temporary (the temporary’s lifetime is extended); an ordinary reference cannot. - A parameter taking
const Point&can be either self or temporary depending on the call; the signature alone does not say which.
Next Step
This lesson measured that a reference works directly with the caller’s object, without
copying; the next and final lesson measures why an operator (operator+) returns a new
temporary, and why operator+= instead returns *this by reference.
To keep your progress and take notes, Log in
My notes
Log in to take notes.