C++ Part I · Module 6 · Functions
Passing by Reference
Chapter 6.2 ended with a limitation stated as plainly as it can be: a by-value parameter is a separate object at a separate address, so a function cannot change what its caller handed it. For most functions that is a feature. For the ones whose entire job is to change something — reset a counter, fill a buffer, swap two variables, sort a collection — it is the whole problem, because the function is working on a copy that is destroyed the instant it returns. This chapter is the direct answer, and it is one character long. Adding an ampersand to a parameter's type stops the copy from being made and makes the parameter another name for the object the caller already has. That phrase is worth taking literally rather than as a figure of speech, and the way to see that it is literal is to run Chapter 6.2's measurement again. The same program, the same two print statements, one extra character in one place — and instead of two addresses differing by forty-four bytes, the output contains the same address twice. The chapter stays narrow on purpose: references have a full treatment in Module 8, and what belongs here is references as a way of declaring a parameter.
Foundation16 min readC++Part IFundamentalsFunctions
Part I · Module 6 · Chapter 6.4 · Passing by Reference
1. One Character
Here is Chapter 6.2's function with one character added:
static void takes_a_copy(int n) // 6.2: a separate object
static void takes_a_reference(int& n) // 6.4: another name for the caller's objectint& is read "reference to int". As a parameter type it means: do not make a new object; let n be another name for whatever the caller passed.
You have already seen this work. Chapter 5.6 §5 measured for (int& d : delays) modifying an array where for (int d : delays) left it alone, and described int& d as making d another name for the element. This is the same mechanism in the other place it appears.
2. The Same Measurement, a Different Answer
Chapter 6.2 §3's program, unchanged except for that one character:
#include <iostream>
static void takes_a_reference(int& n)
{
std::cout << " inside, parameter n = " << n
<< " at address " << static_cast<const void*>(&n) << '\n';
n = 99;
}
int main()
{
int cycles = 7;
std::cout << "before: cycles = " << cycles
<< " at address " << static_cast<const void*>(&cycles) << '\n';
takes_a_reference(cycles);
std::cout << "after: cycles = " << cycles << '\n';
return 0;
}Measured output:
before: cycles = 7 at address 0x16f3728b8
inside, parameter n = 7 at address 0x16f3728b8
after: cycles = 99Put the two experiments side by side, because the contrast is the entire chapter. These are two separate runs, so — per Chapter 6.2 §3's caveat — compare each column against itself, never one column's numbers against the other's:
6.2, int n | 6.4, int& n | |
|---|---|---|
| caller's address | 0x16dc328a8 | 0x16f3728b8 |
| parameter's address | 0x16dc3287c | 0x16f3728b8 |
| same object? | no | yes |
| caller's value after | 7 | 99 |
Within one run, the same address twice. Not a copy that happens to hold the same value — the same storage, reached by two names. n = 99 assigned to cycles because n is cycles for the duration of the call.
A reference parameter is another name for the caller's object. Nothing is copied, and there is no second object to be destroyed when the function returns.
3. What This Is For
A function whose job is an effect rather than a result now has a signature that can express it:
static void reset(int& counter) { counter = 0; }
static void swap_values(int& a, int& b)
{
const int temp = a;
a = b;
b = temp;
}swap_values is the clearest case in the language. It has nothing to return — its entire purpose is that the caller's two variables have exchanged values — and with by-value parameters it is impossible to write. Chapter 6.2 §7 listed it as the example that cannot be solved by returning something.
The same applies to a function that fills something the caller owns, and to Chapter 6.3 §7's out-parameter, which was a reference parameter all along:
static void by_out_param(int a, int b, int& result) { result = a + b; }Chapter 6.3 could not avoid showing that, because an out-parameter has no other mechanism. This chapter is where it is explained.
4. The Signature Is the Documentation
An ampersand in a parameter list is a statement to every caller, and it is the only statement they get.
void configure(int mode, int& status);A reader of that declaration knows mode is read and status may be written, without reading the body. That is Chapter 6.1's contract idea doing real work — and it is why the form matters beyond mechanics.
It also means the call site tells you nothing:
configure(2, result); // does this change result? you cannot tellBoth arguments look identical at the call. Whether result is modified is determined by the declaration, which may be in another file. Chapter 8.6 will contrast this with the pointer form, where the caller writes &result and the possibility of modification is visible at the call site — a real trade-off that cannot be discussed properly until pointers exist.
For now the consequence is a naming discipline: a function that modifies a reference parameter should have a name that says so. reset, fill, swap_values, accumulate_into — a reader who cannot see the declaration can at least read the verb.
5. What a Reference Parameter Will Not Accept
A non-const reference parameter is a request to modify the caller's object, and the language enforces that the caller actually has one.
A literal is refused:
static void f(int& n) { n = 1; }
// ...
f(7);error: no matching function for call to 'f'
note: candidate function not viable: expects an lvalue for 1st argumentA const object is refused:
const int c = 7;
f(c);note: candidate function not viable: 1st argument ('const int') would lose const qualifierBoth refusals are correct and both are useful.
The second is straightforward: f promises to write to its argument, c promises not to change, and the compiler will not let one promise break the other. That is Chapter 3.9's const being load-bearing rather than decorative.
The first note uses a word this curriculum has deliberately not defined. An lvalue, for now, is an expression that names an object which already exists — a variable, for instance. 7 does not name an object; it is a value with nowhere to live, so there is nothing for a reference to be another name for. Writing to it would have no meaning, which is why the call is rejected rather than quietly made to work.
The full account is Chapter 13.6's, and it arrives there because that is where it becomes necessary rather than merely interesting. Chapter 4.1 §6 made the same deferral. The working rule is enough here:
A non-
constreference parameter binds only to a non-constobject that already exists. Not to a literal, not to aconstobject, not to the result of a calculation.
That last one is the restriction that bites in practice:
f(a + b); // refused: a + b names no objectIf the function needs to modify something, there has to be a something. Chapter 6.5 is about the case where you only want to read, and it removes every one of these restrictions — which is most of the reason it exists.
6. The Cost It Removes
Chapter 6.2 §5 measured a 16 KB aggregate being copied on every by-value call. A reference parameter copies nothing:
static void zero_all(SampleBlock& block)
{
for (int& v : block.sample) v = 0;
}That takes the caller's 16 KB object by name and writes to it, with no copy in or out. Note the inner int& v as well — Chapter 5.6's by-reference loop variable, needed for the same reason at the element level.
A reference parameter is cheap regardless of the type's size: it costs about what passing an address costs, which Chapter 8.1 will make precise. The practical rule for now is that the size of a type is not a reason to avoid passing it to a function — only a reason to avoid passing it by value.
7. What to Actually Do
Use a non-const reference parameter when modifying the caller's object is the function's purpose — and only then.
Name the function after the effect. reset, fill, swap_values. The call site cannot show that an argument is modified, so the name has to.
Prefer returning a value when the function computes something — Chapter 6.3 §8.
Do not use a non-const reference just to avoid a copy. Chapter 6.5 is that form.
Keep the reference parameters few and early in the parameter list, so a reader scanning a declaration finds the outputs together.
Expect the compiler to refuse literals and const objects. That refusal is the form working correctly.
8. Mistakes Worth Making Once
Forgetting the & when modification was the point. Measured in 6.2: the caller's value was unchanged, and nothing warned.
Using a non-const reference to avoid a copy. It works and it declares an intent you do not have — Chapter 6.5.
Passing a literal to a reference parameter. expects an lvalue for 1st argument.
Passing a const object to a non-const reference. would lose const qualifier.
Passing the result of a calculation. f(a + b) names no object, so there is nothing to bind.
Assuming the call site shows the modification. It does not; only the declaration does.
Reading "reference" as "pointer". They are different things with different rules, and Chapter 8.6 compares them once pointers exist.
9. Interview Relevance
"What is a reference?" The answer to give is the one this chapter measured: another name for an existing object. Then the evidence — a reference parameter has the same address as the caller's argument, where a by-value parameter does not.
"When would you pass by non-const reference?" When modifying the caller's object is the function's job. If the goal is only to avoid a copy, the answer is const&, and saying so unprompted is the signal that you know the difference between the mechanism and the intent.
"What can a non-const reference not bind to?" A literal, a const object, or a temporary result. A candidate who knows this usually knows it because the compiler taught them, which is the right way to know it.
10. What You Should Be Able to Say Now
int&as a parameter type means do not copy; let the parameter be another name for the caller's object.- Measured: the caller's
cyclesand the parameternwere both at0x16f3728b8— the same address. - So
n = 99leftcyclesat 99. Contrast Chapter 6.2, where the addresses differed andcyclesstayed 7. - The difference in the source is one character.
- This is the same mechanism as Chapter 5.6's
for (int& d : delays). - It is what makes
swap_valuesand every out-parameter possible — including Chapter 6.3 §7's. - The ampersand in the declaration is the only notice a caller gets; the call site looks identical either way.
- So name the function after its effect, and Chapter 8.6 will contrast the pointer form that is visible at the call site.
- A non-
constreference will not bind to a literal:expects an lvalue for 1st argument. - Nor to a
constobject:1st argument ('const int') would lose const qualifier. - An lvalue, provisionally, is an expression naming an object that already exists; Chapter 13.6 is the full account.
- Nor to a calculation —
f(a + b)names no object. - A reference parameter copies nothing regardless of size, so size is a reason to avoid by-value, not a reason to avoid passing.
- It costs the caller's guarantee and the compiler's help — which is why Chapter 6.5 is the form to reach for when you only need to read.
11. Exercises
Exercise 1 — predict the output. Give both addresses' relationship and the final value of cycles for each: (a) void f(int n) { n = 99; } (b) void f(int& n) { n = 99; } (c) void f(int& n) { int m = n; m = 99; }.
Exercise 2 — write the swap. Write swap_values and say in one sentence why it cannot be written with by-value parameters.
Exercise 3 — which will not compile? For void f(int& n) with int a = 1; const int c = 2;, say which calls are rejected and give the note: (a) f(a) (b) f(c) (c) f(3) (d) f(a + 1) (e) f(a + 0).
Exercise 4 — read the signature. For void update(int mode, int& count, int limit);, say what a caller learns about each parameter, and what the call update(1, n, 8) tells a reader who cannot see the declaration.
Exercise 5 — name it honestly. Each of these modifies its reference parameter and is badly named. Give a better name: (a) void value(int& x) (b) void check(int& errors) (c) void data(SampleBlock& b).
Exercise 6 — the wrong mechanism. A colleague writes double mean(SampleBlock& block) to avoid a 16 KB copy. Say what is right about it, what it wrongly declares, and the one-word fix.
Exercise 7 — choose. For each, choose a return value, a non-const reference parameter, or neither-yet, and justify: (a) add two numbers (b) zero a 16 KB block (c) read the mean of a 16 KB block (d) exchange two variables (e) report whether a parse succeeded and produce the parsed value.
12. Summary
int&means another name for the caller's object, not a copy.- Measured: the same address twice (
0x16f3728b8), and the caller's value became 99. - The difference from Chapter 6.2 is one character; the difference in meaning is total.
- Same mechanism as Chapter 5.6's
for (int& d : delays). - It is what makes
swap_valuesand out-parameters possible. - Only the declaration shows it — the call site looks the same — so name the function after its effect.
- It will not bind to a literal, a
constobject, or a calculation; the notes sayexpects an lvalueandwould lose const qualifier. - An lvalue here means an expression naming an existing object; Chapter 13.6 defines it properly.
- It copies nothing regardless of size.
- It costs the caller's guarantee and the compiler's help — so use it only when modifying is the job.
The restriction list in §5 and the two costs in §6 point at the same missing form. Nearly every time you want to avoid a copy, you want to read a large object, not change it — and this chapter's parameter type says the opposite, refuses perfectly reasonable arguments, and gives the compiler nothing to check. The next chapter adds one word and gets all of it back: no copy, a promise the compiler enforces, and arguments that no longer have to be variables. It is the single most common parameter form in real C++, and by the end of it you will be able to say why each of those three properties follows from the same word.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
