C++ Part I · Module 6 · Functions
Parameters, Arguments and Pass-by-Value
This is the chapter the rest of the curriculum leans on. The question it answers is one sentence long — what happens to the value you hand a function — and the answer is also one sentence: it is copied. The reason that deserves a whole chapter, and the reason this one is marked Critical, is that almost every difficulty in Modules 8, 12 and 13 is a consequence of it. A pointer is interesting because copying an address is not the same as copying what it points at. A copy constructor exists because some types cannot be copied correctly by duplicating their bytes. Move semantics exist because some copies are pure waste. None of those make sense to someone who is vague about what an ordinary call already does. So rather than assert the rule, this chapter measures it twice. First with an int, where printing two addresses settles the matter beyond argument: the caller's object and the parameter are not in the same place, so they are not the same object. Then with something large, where the same measurement shows what the copy costs — and sets up the limitation that the next three chapters exist to remove.
Foundation17 min readC++Part IFundamentalsFunctions
Part I · Module 6 · Chapter 6.2 · Parameters, Arguments and Pass-by-Value
1. Two Words That Are Often Swapped
Precision here saves confusion later, because the rest of the chapter is about the relationship between the two.
static int twice(int n) { return 2 * n; } // n is a PARAMETER
twice(cycles); // cycles is an ARGUMENTA parameter is the variable in the function's own declaration. It is part of the contract from Chapter 6.1, and it exists inside the function.
An argument is the expression you supply at the call site. It exists in the caller.
The question this chapter answers is what connects them.
2. A Parameter Is an Object
The answer begins with something Chapter 3.1 already established. A parameter is a variable, which means it is a name for an object, which means it is typed storage with a lifetime of its own.
That object is created when the call starts and destroyed when the function returns, and it is initialized from the argument. Chapter 3.7's initialization rules apply — a parameter is initialized, not assigned.
So a call does two things, in order:
- Evaluate the argument expression.
- Initialize the parameter object from the result.
Step 2 is where the copy happens, and §3 proves it.
3. The Copy, Proved by Address
Assertions about copying are easy to make and easy to doubt. Here is the measurement instead. Both the caller and the function print where their object lives:
#include <iostream>
static void takes_a_copy(int n)
{
std::cout << " inside, parameter n = " << n
<< " at address " << static_cast<const void*>(&n) << '\n';
n = 99;
std::cout << " inside, after n = 99 = " << n << '\n';
}
int main()
{
int cycles = 7;
std::cout << "before: cycles = " << cycles
<< " at address " << static_cast<const void*>(&cycles) << '\n';
takes_a_copy(cycles);
std::cout << "after: cycles = " << cycles << '\n';
return 0;
}Measured output:
before: cycles = 7 at address 0x16dc328a8
inside, parameter n = 7 at address 0x16dc3287c
inside, after n = 99 = 99
after: cycles = 7Read the two addresses. 0x16dc328a8 and 0x16dc3287c are different, by 44 bytes. There are two objects.
The exact values will not match on your machine, and will not match between two runs on this one. Where the stack sits is an implementation detail, and operating systems deliberately move it. The claim is never a particular number — it is the relationship between two numbers printed by the same run. Run it twice and both addresses change; that they differ from each other does not.
Everything else follows from that:
nstarted at 7 because it was initialized fromcycles.- Setting
n = 99changedn, and the second line inside the function confirms it. cyclesis still 7 afterwards, becausenwas nevercycles.
An argument is copied into the parameter. The parameter is a separate object at a separate address, so assigning to it cannot change the caller's object.
That sentence is the one the module blurb calls load-bearing. It is worth being able to produce the measurement, not just the claim.
On the addresses themselves. &cycles is the address-of operator, and printing it through static_cast<const void*> is what makes the stream show a number rather than interpreting it as text. Both belong to Chapter 8.1, which is where pointers are the subject. Here an address is being used as nothing more than an identity test: two objects at two addresses are two objects.
4. What the Compiler Says About Parameters You Ignore
A small measured aside, because it is a flag fact worth owning.
A parameter you never use is usually a sign that the contract and the implementation have drifted apart. The warning for it is not in -Wall:
static int f(int n) { return 0; }$ c++ -std=c++17 -Wall f.cpp -o f
$ ← zero warnings
$ c++ -std=c++17 -Wall -Wextra f.cpp -o f
f.cpp:1:18: warning: unused parameter 'n' [-Wunused-parameter]This curriculum has compiled with -Wall -Wextra -Wpedantic since Chapter 1, so you have had this warning all along. It is worth knowing which flag it came from, because -Wall alone is a common project default and it does not include this.
When a parameter genuinely must exist and genuinely is not used — a signature fixed by somebody else's interface — (void)n; silences it and records that the omission is deliberate. That is the same "say you meant it" pattern as Chapter 5.3's [[fallthrough]].
5. What the Copy Costs
For an int, the copy in §3 is four bytes and nothing to think about. The purpose of this chapter includes the case where it is not, so here is something larger.
The reader has not met a type whose copy is expensive — those arrive with Chapter 9.5's std::string and the containers of Module 11. So this example borrows one construct from Chapter 9.7, a struct, which groups several values into one type:
#include <iostream>
struct SampleBlock { int sample[4096]; }; // Chapter 9.7's subject, borrowed
static long long sum_by_value(SampleBlock block)
{
std::cout << " by value: parameter at " << static_cast<const void*>(&block) << '\n';
long long total = 0;
for (int v : block.sample) total += v;
return total;
}
static long long sum_by_const_ref(const SampleBlock& block)
{
std::cout << " by const&: parameter at " << static_cast<const void*>(&block) << '\n';
long long total = 0;
for (int v : block.sample) total += v;
return total;
}
int main()
{
std::cout << "sizeof(int) = " << sizeof(int) << " bytes\n";
std::cout << "sizeof(SampleBlock) = " << sizeof(SampleBlock) << " bytes\n";
SampleBlock block{};
for (int i = 0; i < 4096; ++i) block.sample[i] = i;
std::cout << "caller's block at " << static_cast<const void*>(&block) << '\n';
sum_by_value(block);
sum_by_const_ref(block);
return 0;
}struct SampleBlock { int sample[4096]; }; is a type holding 4096 ints. That is all this chapter needs from Chapter 9.7 — the declaration and reaching a member with .. Layout and padding are Chapter 9.8's, and the loop over block.sample is Chapter 5.6's range-based for.
Measured output, sizes first:
sizeof(int) = 4 bytes
sizeof(SampleBlock) = 16384 bytesand then the addresses:
caller's block at 0x16ddf286c
by value: parameter at 0x16ddee868
by const&: parameter at 0x16ddf286cThe by-value parameter is at a different address, so 16,384 bytes were copied to produce it — on every call. The const& parameter is at the caller's own address, so nothing was copied at all; the function is working on the caller's object.
That second line is Chapter 6.5's whole subject and it appears here only to make the cost concrete. The point this chapter is making is the first line: by value, a 16 KB argument costs 16 KB per call.
6. How Much That Actually Matters
Size is not the same as time, so here it is timed: 200,000 calls of each form, compiled at -O2, with the results accumulated so nothing can be optimised away.
Five consecutive runs:
| run | by value | by const& |
|---|---|---|
| 1 | 62 ms | 29 ms |
| 2 | 62 ms | 29 ms |
| 3 | 63 ms | 29 ms |
| 4 | 63 ms | 30 ms |
| 5 | 63 ms | 30 ms |
So roughly twice the time, consistently, for the same work.
Three honest qualifications, because a benchmark that is quoted without them is worse than none.
The first run of a fresh process gave 82 ms for the by-value version before settling at 62–63. That is a cold-start artefact, and quoting 82 as "the result" would have overstated the gap by a third.
The ratio is specific to this program. It depends on the size of the object, on how much work the body does per call, and on the compiler and machine. A function doing substantial work per call would show a smaller ratio; a larger object would show a bigger one. Chapter 1.3 §5's rule applies to measurements as much as to behaviour: this is what happened here, not what must happen everywhere.
The deterministic claim is the address, not the time. §5's two addresses are a fact about the language. The milliseconds are a fact about one afternoon on one laptop. When you need to convince somebody — including yourself — that a copy is happening, print the address.
7. The Limitation, Stated Plainly
Pass-by-value has exactly one consequence that is a problem, and it is the one §3 measured: a function cannot change its caller's object.
Sometimes that is precisely what you want. A function that computes something from its inputs and leaves them alone is easier to reason about, easier to test, and cannot surprise its caller. Chapter 6.9 argues that this should be the default.
But sometimes changing the caller's object is the job. A function that resets a counter, fills a buffer, or sorts a collection has nothing to return — its entire purpose is the effect on what it was given, and with a by-value parameter it is working on a copy that is destroyed the moment it returns.
There are three ways out, and they are the next three chapters:
| Need | Answer | Chapter |
|---|---|---|
| Give the caller a new value | return it | 6.3 |
| Change the caller's object | take a reference | 6.4 |
| Read a large object without copying | take a const reference | 6.5 |
Chapter 5.6 §5 already showed the middle one working, in a different setting: for (int& d : delays) modified the array where for (int d : delays) did not. Same mechanism, same one character, now at a function boundary.
8. What to Actually Do
Pass by value for the fundamental types. An int, a double, a bool, a char — copying is free and the function gets a value it can use as scratch without affecting anybody.
Treat a by-value parameter as yours. Modifying it is legitimate and local. It is often the clearest way to write a loop that consumes a count.
Do not pass a large object by value when you only need to read it. Chapter 6.5 is the form you want, and §6 is what it saves.
Do not expect a by-value parameter to be an output. If the function's job is to change something, say so in the signature — Chapter 6.4.
Keep -Wextra on for -Wunused-parameter, and write (void)n; when a parameter must exist unused.
Print an address when you need to know whether a copy happened. It is deterministic where timing is not.
9. Mistakes Worth Making Once
Expecting a function to change its by-value parameter's argument. Measured: cycles was still 7.
Explaining it as "the function got a copy of the value". True but weak. The sharper statement is that the parameter is a different object at a different address, which is what makes the rest of Part I follow.
Passing a large object by value out of habit. Measured: 16,384 bytes per call, and roughly twice the time.
Quoting a benchmark's first run. Measured: 82 ms cold, 62–63 ms settled.
Treating the timing ratio as a property of C++. It is a property of this program on this machine.
Assuming -Wall covers unused parameters. It does not; -Wextra does.
Confusing parameter with argument. The parameter is in the declaration; the argument is at the call site.
10. Interview Relevance
"Is C++ pass-by-value or pass-by-reference?" is a question with a trap in it, and the trap is that the honest answer is "pass-by-value, and references are a parameter type you can ask for".
The strong answer says: arguments are copied into parameters by default; a reference parameter is how you opt out; and passing a pointer by value is still pass-by-value — the pointer is copied, which is why you can change what it points at but not the caller's pointer.
The follow-up is usually about cost. Say that a by-value parameter of a large type copies the whole object on every call, that for the fundamental types this is free and fine, and that const& is the standard way to take something large for reading. If you can add how you would demonstrate a copy — print the parameter's address and the argument's address and compare them — you have answered a question most candidates answer from memory.
11. What You Should Be Able to Say Now
- A parameter is in the function's declaration; an argument is at the call site.
- A parameter is an object — typed storage with a lifetime — created at the call and destroyed at the return.
- A call evaluates the argument, then initializes the parameter from it. The initialization is the copy.
- Measured: caller's
cyclesat0x16dc328a8, parameternat0x16dc3287c— different addresses, two objects. - So setting
n = 99leftcyclesat 7. A by-value parameter cannot change the caller's object. -Wunused-parameteris in-Wextra, not-Wall— measured both ways.(void)n;records a deliberate omission.- Measured:
sizeof(SampleBlock)with 4096ints is 16384 bytes, copied on every by-value call. - Measured: the by-value parameter had a different address; the
const¶meter had the caller's own address — no copy at all. - Measured: 200,000 calls took 62–63 ms by value against 29–30 ms by
const&, roughly twice. - The first run gave 82 ms — a cold-start artefact, and the reason to report a range rather than a number.
- The address is the deterministic claim; the timing is one machine on one day.
- This rule is what Modules 8, 12 and 13 are built on: copying a pointer, running a constructor and destructor per call, and the double-ownership bug of Chapter 13.4.
- The one limitation — cannot change the caller's object — has three answers: return a value (6.3), take a reference (6.4), take a
constreference (6.5).
12. Exercises
Exercise 1 — name them. In double mean(int a, int b) called as mean(total, count), name the parameters, name the arguments, and say which exist inside the function.
Exercise 2 — predict the output. Give the output of §3's program if takes_a_copy is changed to n = n * 2;, including both addresses' relationship to each other.
Exercise 3 — prove it yourself. Write the smallest complete program that proves a by-value parameter is a different object from its argument, without using the words "copy" or "value" in any comment.
Exercise 4 — what does it cost? For each parameter type, say how many bytes a by-value call copies and whether you would pass by value: (a) int (b) double (c) bool (d) a struct of 4096 ints (e) a struct of two ints.
Exercise 5 — read the benchmark. §6 reports 62–63 ms and 29–30 ms, and mentions an 82 ms first run. State precisely what the measurement establishes, what it does not, and which single number in the chapter is a fact about C++ rather than about one machine.
Exercise 6 — which flag? A project compiles with -Wall and a reviewer asks why an unused parameter was not caught. Give the flag, and give the one-line change that records a deliberately unused parameter.
Exercise 7 — look ahead. For each goal, name which of §7's three answers you need and why by-value cannot do it: (a) double a number and give the result back (b) zero a buffer the caller owns (c) compute the mean of 4096 samples without copying them (d) swap two of the caller's variables.
13. Summary
- A parameter lives in the declaration and inside the function; an argument lives at the call site.
- A parameter is an object, initialized from the argument — that initialization is the copy.
- Measured: the two addresses differ, so
n = 99leftcyclesat 7. - A by-value parameter cannot change the caller's object. That is the rule Modules 8, 12 and 13 assume.
-Wunused-parameteris-Wextra, not-Wall;(void)n;says the omission is deliberate.- Measured: a 4096-
intstruct is 16384 bytes and is copied on every by-value call; theconst¶meter shared the caller's address. - Measured: roughly twice the time (62–63 ms against 29–30 ms), with an 82 ms cold first run not quoted as the result.
- The address is deterministic; the timing is not. Print an address to prove a copy.
- Pass fundamental types by value and treat the parameter as scratch.
- The one limitation has three answers: return (6.3), reference (6.4),
constreference (6.5).
The first of those three is the one that needs no new machinery at all, and it is where most functions should end up: compute something, hand it back, touch nothing the caller owns. The next chapter is about doing that correctly — including what the compiler says when a path through the function forgets to, what happens when you return a reference to something that is about to cease existing, and a C++17 attribute that makes ignoring a result a warning instead of a silent mistake.
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.
