Skip to content
academia.sh

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 (an int, a Point of two ints); for large or expensively copied types (a class managing its own memory, like Buffer in 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. nudge does 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 was const 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 == &p is 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 const reference 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close