SECTION 1
Historical Context & Motivation
The question of how data flows between a caller and a called subroutine is as old as programming itself. In the earliest assembly-language programs of the 1940s and 1950s, programmers managed this manually—pushing values onto hardware stacks, loading addresses into registers, or even self-modifying code to store return values. As higher-level languages emerged, designers had to formalize these ad-hoc conventions into parameter-passing semantics, and the two most influential strategies—pass-by-value and pass-by-reference—crystallized across different language families during the 1950s through 1970s.
1957 • FORTRAN and Call-by-Reference
1960 • ALGOL 60 Introduces Call-by-Value
1972 • C Chooses Value Semantics
1979–1985 • C++ Adds Reference Parameters
2011 • C++11 and Move Semantics
C++ inherits C's default of passing by value but layers on native reference syntax, giving the programmer fine-grained control. The central question this lesson addresses is: when should a function receive a copy of its argument, and when should it receive an alias to the original? Answering this correctly is fundamental to writing functions that are correct, efficient, and clear in intent.
SECTION 2
Core Principles & Definitions
Understanding the distinction between pass-by-value and pass-by-reference requires clarity about what happens in memory when a function is invoked. Every function call creates a new stack frame (also called an activation record) that holds space for the function's local variables and parameters. The parameter-passing mechanism determines the relationship between the data in the caller's stack frame and the data in the callee's stack frame.
Pass-by-Value
& is present in the parameter declaration.Pass-by-Reference
T& param in the function signature.Const Reference
const T& param, this hybrid strategy passes an alias (avoiding the cost of copying) but prohibits modification. It combines the efficiency of pass-by-reference with the safety guarantee of pass-by-value, and is the idiomatic choice for large read-only arguments in C++.Stack Frame Isolation
KEY TAKEAWAY
SECTION 3
Visual Explanation — Memory Model
The following diagram illustrates what happens in memory when a function is called with pass-by-value versus pass-by-reference. On the left, the caller passes x by value to increment(int n): a separate copy n is created. On the right, the caller passes x by reference to increment(int& n): the parameter n is an alias pointing back to x.
n at a different address (0x7fa0). Incrementing n leaves x unchanged. Right: pass-by-reference makes n an alias to x (same address 0x7ff0). Incrementing n directly modifies x.The key visual insight is the arrow direction and target. In the pass-by-value scenario, data flows down from the caller into a fresh memory slot in the callee's frame—a one-way copy. In pass-by-reference, the dashed arrow loops back up to the caller's original variable, establishing a bidirectional connection. Under the hood, the compiler typically implements a reference as a pointer that is automatically dereferenced, but the language semantics guarantee alias behavior without exposing raw pointer arithmetic to the programmer.
SECTION 4
How It Works — Compiler Mechanics
At the machine level, both parameter-passing mechanisms involve operations on the call stack. When a function is invoked, the compiler generates instructions to prepare arguments according to the calling convention (e.g., System V AMD64 ABI on Linux places the first several integer/pointer arguments in registers rdi, rsi, rdx, etc.). The mechanism chosen—value or reference—determines what is placed into those registers or onto the stack.
Pass-by-Value: Copy Construction
When you declare void foo(int n), calling foo(x) causes the compiler to copy the value of x into the storage for n. For primitive types, this is a simple bitwise copy—extremely cheap. For class types, the copy constructor is invoked, which may allocate heap memory, duplicate internal buffers, or perform deep copies. This is where pass-by-value can become expensive: copying a std::vector<int> of one million elements requires O(n) time and memory.
COPY COST (VALUE SEMANTICS)
For trivially copyable types (int, double, pointers), the copy is a constant-time register/memory operation. For complex objects like std::string or std::vector, cost scales with the size of the owned resources.
Pass-by-Reference: Address Indirection
When you declare void foo(int& n), calling foo(x) causes the compiler to pass the address of x rather than its value. Internally, the generated assembly is virtually identical to passing a pointer: the address is loaded into a register, and every use of n inside the function is translated into a memory dereference through that address. The cost is always O(1)—just the size of a pointer (typically 8 bytes on a 64-bit system)—regardless of how large the underlying object is.
REFERENCE COST
The cost of passing a reference is constant regardless of the size of the referred-to object. However, each access through the reference incurs an indirection (pointer dereference), which may cause a cache miss if the original object is not in the CPU cache.
Reference vs. Pointer — A Syntactic Distinction
T&) and a C pointer (T*) both convey an address at the machine level. The differences are purely at the language level: references must be initialized at declaration, cannot be reseated to refer to a different object, and cannot be null. These constraints make references safer and semantically cleaner than raw pointers for parameter passing.SECTION 5
Detailed Breakdown — When to Use Which
Choosing between pass-by-value, pass-by-reference, and pass-by-const-reference is one of the most important interface design decisions in C++. The choice affects correctness (can the function modify the argument?), performance (how much does copying cost?), and expressiveness (what does the signature communicate to readers?). The following decision matrix codifies the idiomatic guidelines recommended by the C++ Core Guidelines (maintained by Bjarne Stroustrup and Herb Sutter).
T&. If not, ask whether the type is cheap to copy (roughly ≤ 16 bytes and trivially copyable). If yes, pass by value; if no, pass by const T&.| Type | sizeof (typical, 64-bit) | Recommended Passing |
|---|---|---|
int | 4 bytes | By value |
double | 8 bytes | By value |
char | 1 byte | By value |
std::pair<int,int> | 8 bytes | By value |
std::string | 32 bytes + heap | By const reference (read) or by value (sink) |
std::vector<T> | 24 bytes + heap | By const reference (read) or by reference (modify) |
std::map<K,V> | 48 bytes + heap | By const reference (read) or by reference (modify) |
Common C++ types and their idiomatic parameter-passing strategies
SECTION 6
Worked Example — Swap Function
The classic example that distinguishes pass-by-value from pass-by-reference is the swap function. A swap function must exchange the values of two variables in the caller's scope. Let us trace through both an incorrect (value) and correct (reference) implementation to understand exactly why references are essential here.
Implementing swap — Value vs. Reference
- 1
Step 1 — Attempt with Pass-by-Value (Incorrect)
Consider the function:void swapBad(int a, int b) { int temp = a; a = b; b = temp; }. We call it withint x = 3, y = 7; swapBad(x, y);. At the call site, the values 3 and 7 are copied into parametersaandb. The function successfully swapsaandbwithin its own stack frame—but these are local copies.AfterswapBadreturns: x = 3, y = 7 (unchanged) - 2
Step 2 — Trace the Stack Frames
Before the call: main's frame has x = 3 at 0x7ff0 and y = 7 at 0x7ff4. During the call: swapBad's frame creates copies a = 3 at 0x7fa0 and b = 7 at 0x7fa4. The swap executes: temp = 3, a = 7, b = 3. When swapBad returns, its frame is destroyed. main's x and y are untouched. - 3
Step 3 — Correct Implementation with References
Now consider:void swapGood(int& a, int& b) { int temp = a; a = b; b = temp; }. We callswapGood(x, y);. Nowais an alias forx(both refer to 0x7ff0) andbis an alias fory(both refer to 0x7ff4). The assignmenta = bwrites 7 into address 0x7ff0 (which is x), andb = tempwrites 3 into address 0x7ff4 (which is y).AfterswapGoodreturns: x = 7, y = 3 (swapped!) - 4
Step 4 — Generalize with Templates
In practice, the C++ standard library providesstd::swap, which is a template function:template<typename T> void swap(T& a, T& b) { T temp = std::move(a); a = std::move(b); b = std::move(temp); }. Note that it uses references (not values) and leverages move semantics for efficiency with complex types. Without the&on the parameters, this template could never work. - 5
Step 5 — const Reference Example (Read-Only)
Now consider a function that only reads its argument:void printLength(const std::string& s) { std::cout << s.size(); }. Usingconst std::string&avoids copying the entire string (which could be megabytes) while guaranteeing the function cannot modify the caller's string. Attemptings = "new";inside the function would produce a compiler error.const T& = efficient read-only access, enforced by the compiler
SECTION 7
Strengths & Limitations Comparison
Neither pass-by-value nor pass-by-reference is universally superior. Each has contexts where it excels and contexts where it introduces risks or inefficiencies. A mature C++ programmer selects the mechanism deliberately, treating the function signature as a contract that communicates intent, ownership, and mutation guarantees to both the compiler and future readers of the code.
| Criterion | Pass-by-Value | Pass-by-Reference (&) | Pass-by-Const-Ref (const &) |
|---|---|---|---|
| Mutation of original | Impossible — callee has a copy | Possible and intended | Impossible — compiler enforced |
| Copy cost | Full copy (O(n) for containers) | No copy — O(1) address | No copy — O(1) address |
| Safety | High — caller's state is protected | Lower — unintended side effects possible | High — no mutation allowed |
| Aliasing concerns | None — independent copy | Yes — multiple refs can alias the same object | Reduced — but aliasing still possible |
| Can bind to rvalue/literal | Yes — copies the temporary | No — requires an lvalue | Yes — lifetime extension for temporaries |
| Thread safety | Natural — no shared state | Requires synchronization | Safer but not immune to data races |
Comparative analysis of C++ parameter-passing strategies
DESIGN PRINCIPLE
const T& for inputs and return by value for outputs—this maximizes both safety and clarity.SECTION 8
Connection to Move Semantics & Smart Pointers
The pass-by-value vs. pass-by-reference dichotomy is foundational, but modern C++ (C++11 and beyond) extends the conversation with rvalue references (T&&) and move semantics. These mechanisms address a limitation of the original two strategies: what if you want to transfer ownership of a resource from the caller to the callee without the cost of a deep copy? Move semantics allow the callee to "steal" the internal resources (e.g., the heap buffer of a std::vector) from a temporary or an object the caller explicitly marks as expendable via std::move.
| Concept | This Lesson (Fundamentals) | Advanced C++ Extension |
|---|---|---|
| Passing mechanism | Copy (T) or alias (T&, const T&) | Move (T&&) — transfer ownership without copy |
| Cost for large objects | Value: O(n); Reference: O(1) + indirection | Move: O(1) — pointer swap, no deep copy |
| Ownership model | Caller retains ownership; callee borrows or copies | std::unique_ptr<T> enforces single ownership; move transfers it |
| Forwarding | Must choose value or ref at declaration time | std::forward<T> (perfect forwarding) preserves value category in templates |
| Sink parameter idiom | Pass by value + local use → copy on lvalue, move on rvalue | Overload sets: f(const T&) + f(T&&) for maximum efficiency |
From value/reference fundamentals to advanced C++ ownership semantics
As you advance into template metaprogramming and library design, you will encounter forwarding references (also called universal references), declared as template<typename T> void f(T&& arg). These deduce whether the argument is an lvalue or rvalue and forward it accordingly using std::forward<T>(arg). This mechanism builds directly on the reference semantics covered in this lesson, making a firm understanding of T& vs. value passing an essential prerequisite. The journey from pass-by-value to perfect forwarding represents one of C++'s deepest design trajectories, and mastering the fundamentals here prepares you for that more sophisticated layer.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
void mystery(int a, int& b) { a = a + 10; b = b + 10; }
If x = 5 and y = 5, and we call mystery(x, y), what are the values of x and y after the call returns? Explain why.Show answer
a is passed by value, so it receives a copy of x. Adding 10 to a modifies only the local copy; x in the caller remains 5. The parameter b is passed by reference (int&), so it is an alias for y. Adding 10 to b directly modifies y, changing it from 5 to 15.Question 2: BASIC CALCULATION
void doubleIt(int& val) that doubles the value of its argument in place. Then trace through the following code, showing the value of num after each call:
int num = 4; doubleIt(num); doubleIt(num); doubleIt(num);Show answer
void doubleIt(int& val) { val = val * 2; }. Since val is a reference to num, each call modifies num directly. After the first call: num = 4 × 2 = 8. After the second call: num = 8 × 2 = 16. After the third call: num = 16 × 2 = 32. If val were passed by value (no &), num would remain 4 after all three calls.Question 3: INTERMEDIATE
void clamp(int& value, int lo, int hi) { if (value < lo) value = lo; if (value > hi) value = hi; }
Explain why value must be passed by reference while lo and hi are passed by value. Then, given int score = 105; clamp(score, 0, 100);, what is score after the call? What would happen if all three parameters were passed by reference?Show answer
value must be a reference because the function's purpose is to modify the caller's variable—clamping it into the range [lo, hi]. If value were passed by value, only the local copy would be clamped, and the caller's score would remain 105. The bounds lo and hi are read-only inputs that the function does not need to modify, so pass-by-value is appropriate (and since int is small, it is efficient). After clamp(score, 0, 100), score = 100 because 105 > 100 triggers the second conditional. If all three were references, the function would still work correctly for named variables, but you could not call clamp(score, 0, 100) because 0 and 100 are integer literals (rvalues), and non-const lvalue references cannot bind to rvalues—this would cause a compilation error.Question 4: APPLIED
computeStats that takes a vector of doubles (potentially millions of elements) and computes both the mean and standard deviation. Specify the complete function signature, choosing the appropriate parameter-passing mechanism for each parameter. Justify each choice in terms of correctness and performance.Show answer
void computeStats(const std::vector<double>& data, double& mean, double& stddev). The vector data is passed as const std::vector<double>& because (1) it may contain millions of elements, making a copy prohibitively expensive, and (2) the function only reads the data, so const prevents accidental modification. The outputs mean and stddev are passed as double& (non-const references) because the function must write computed results back to the caller's variables. An alternative modern idiom would be to return a std::pair<double, double> by value, relying on copy elision (NRVO/RVO) to eliminate the copy cost.Question 5: CRITICAL THINKING
void increment(int& a, int& b) { a++; b++; }
A caller invokes int x = 0; increment(x, x);. What is the value of x after the call? Explain the aliasing issue at play. How does this differ from what would happen with pass-by-value? Propose a general strategy to protect against such aliasing bugs in production code.Show answer
a and b are references bound to the same variable x. When a++ executes, x becomes 1. Then b++ increments the same location again, making x = 2. This is an aliasing issue: the function was likely designed assuming a and b refer to distinct objects. With pass-by-value, both parameters would be independent copies initialized to 0, each incremented to 1, and x would remain 0—no aliasing possible. To protect against such bugs in production, several strategies exist: (1) use __restrict__ qualifiers (compiler-specific) to assert non-aliasing, (2) document non-aliasing preconditions in the function contract, (3) use static analysis tools that detect potential aliasing at call sites, or (4) redesign the API so that inputs and outputs are structurally distinct (e.g., return a struct instead of using output references).SUMMARY
Lesson Summary
Summary
C++ provides two fundamental mechanisms for passing arguments to functions. Pass-by-value creates an independent copy of the argument in the callee's stack frame, guaranteeing that the original variable is never modified. Pass-by-reference (declared with &) passes an alias to the caller's variable, enabling the function to modify it directly. Const references (const T&) combine the efficiency of reference passing with the safety of value passing by prohibiting modification.
The decision heuristic is straightforward: use T& when the function must modify the caller's data; use pass-by-value for small, cheap-to-copy types that are read-only; and use const T& for large, expensive-to-copy types that are read-only. Be mindful of aliasing when multiple reference parameters may refer to the same object. These fundamentals serve as the foundation for understanding move semantics (rvalue references, T&&) and perfect forwarding in advanced C++.