C++ • FUNCTIONS AND PROGRAM STRUCTURE

Pass-by-Value vs. Pass-by-Reference — Pass arguments by value vs by reference (&) (conceptual + practice)

Understanding how C++ transmits data to functions and why the choice profoundly affects program behavior.

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
FORTRAN, the first widely-used compiled language, passed all arguments by reference by default. Every parameter was effectively an alias for the caller's variable, meaning subroutines could freely modify the caller's data—a powerful but sometimes dangerous capability.
1960
ALGOL 60 Introduces Call-by-Value
The ALGOL 60 report formalized both call-by-value and call-by-name semantics. Call-by-value copied the argument before the procedure began, guaranteeing that the callee could not accidentally corrupt the caller's state. This was a pivotal conceptual contribution that influenced virtually every subsequent language.
1972
C Chooses Value Semantics
Dennis Ritchie's C language adopted pass-by-value for all parameters, requiring programmers to explicitly use pointers when they needed reference-like behavior. This design traded convenience for safety and explicitness, making the programmer's intent visible in every function signature.
1979–1985
C++ Adds Reference Parameters
Bjarne Stroustrup, extending C into C++, introduced the reference type (T&) beginning with 'C with Classes' in 1979 and standardized in the first edition of The C++ Programming Language (1985). References provided alias semantics without requiring explicit pointer dereferencing, unifying the convenience of FORTRAN-style references with C's type safety.
2011
C++11 and Move Semantics
C++11 introduced rvalue references (T&&) and move semantics, adding a third dimension to argument passing. Move semantics allow efficient transfer of resources without copying, extending the value-vs-reference discussion into the domain of ownership and performance optimization.

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.

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.

1

Pass-by-Value

The function receives an independent copy of the argument. Modifications to the parameter inside the function have no effect on the caller's original variable. In C++, this is the default for all types when no & is present in the parameter declaration.
2

Pass-by-Reference

The function receives an alias to the caller's original variable. The parameter and the argument share the same memory location. Any modification to the parameter directly modifies the original. Declared using T& param in the function signature.
3

Const Reference

Declared as 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++.
4

Stack Frame Isolation

In pass-by-value, the callee's parameter occupies its own slot in its stack frame. Changes are local. In pass-by-reference, the callee's parameter is implemented (under the hood) as a pointer to the caller's variable, so the callee operates directly on the caller's memory.
✦ KEY TAKEAWAY
Think of pass-by-value like handing someone a photocopy of a document—they can scribble on it all they want, and your original stays pristine. Pass-by-reference is like handing them the original document itself—every edit they make is an edit to your copy. A const reference is like handing them the original inside a sealed display case: they can read it but cannot alter it.

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.

Left: pass-by-value creates an independent copy 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.

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)
T_copy(n) = O(sizeof(T)) for trivial types, O(n) for containers of n elements
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
T_ref = O(1) = sizeof(pointer) = 8 bytes on 64-bit architectures
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
A C++ reference (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.

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).

Decision flowchart: if the function must modify the caller's variable, use 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&.
Common C++ types and their idiomatic parameter-passing strategies
Typesizeof (typical, 64-bit)Recommended Passing
int4 bytesBy value
double8 bytesBy value
char1 byteBy value
std::pair<int,int>8 bytesBy value
std::string32 bytes + heapBy const reference (read) or by value (sink)
std::vector<T>24 bytes + heapBy const reference (read) or by reference (modify)
std::map<K,V>48 bytes + heapBy const reference (read) or by reference (modify)

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 with int x = 3, y = 7; swapBad(x, y);. At the call site, the values 3 and 7 are copied into parameters a and b. The function successfully swaps a and b within its own stack frame—but these are local copies.
After swapBad returns: x = 3, y = 7 (unchanged)
2
Step 2 — Trace the Stack FramesBefore 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 ReferencesNow consider: void swapGood(int& a, int& b) { int temp = a; a = b; b = temp; }. We call swapGood(x, y);. Now a is an alias for x (both refer to 0x7ff0) and b is an alias for y (both refer to 0x7ff4). The assignment a = b writes 7 into address 0x7ff0 (which is x), and b = temp writes 3 into address 0x7ff4 (which is y).
After swapGood returns: x = 7, y = 3 (swapped!)
4
Step 4 — Generalize with TemplatesIn practice, the C++ standard library provides std::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(); }. Using const std::string& avoids copying the entire string (which could be megabytes) while guaranteeing the function cannot modify the caller's string. Attempting s = "new"; inside the function would produce a compiler error.
const T& = efficient read-only access, enforced by the compiler

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.

Comparative analysis of C++ parameter-passing strategies
CriterionPass-by-ValuePass-by-Reference (&)Pass-by-Const-Ref (const &)
Mutation of originalImpossible — callee has a copyPossible and intendedImpossible — compiler enforced
Copy costFull copy (O(n) for containers)No copy — O(1) addressNo copy — O(1) address
SafetyHigh — caller's state is protectedLower — unintended side effects possibleHigh — no mutation allowed
Aliasing concernsNone — independent copyYes — multiple refs can alias the same objectReduced — but aliasing still possible
Can bind to rvalue/literalYes — copies the temporaryNo — requires an lvalueYes — lifetime extension for temporaries
Thread safetyNatural — no shared stateRequires synchronizationSafer but not immune to data races
⚙ DESIGN PRINCIPLE
In concurrent and multi-threaded systems (a staple of modern software engineering), pass-by-value provides natural isolation between threads since each thread operates on its own copy. Pass-by-reference requires explicit locking or atomic operations to avoid data races. This mirrors the broader software engineering principle that shared mutable state is the root of most concurrency bugs. When designing APIs, prefer const T& for inputs and return by value for outputs—this maximizes both safety and clarity.

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.

From value/reference fundamentals to advanced C++ ownership semantics
ConceptThis Lesson (Fundamentals)Advanced C++ Extension
Passing mechanismCopy (T) or alias (T&, const T&)Move (T&&) — transfer ownership without copy
Cost for large objectsValue: O(n); Reference: O(1) + indirectionMove: O(1) — pointer swap, no deep copy
Ownership modelCaller retains ownership; callee borrows or copiesstd::unique_ptr<T> enforces single ownership; move transfers it
ForwardingMust choose value or ref at declaration timestd::forward<T> (perfect forwarding) preserves value category in templates
Sink parameter idiomPass by value + local use → copy on lvalue, move on rvalueOverload sets: f(const T&) + f(T&&) for maximum efficiency

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.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Consider the following code: 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.
PROBLEM 2 — BASIC CALCULATION
Write a function 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);
PROBLEM 3 — INTERMEDIATE
Consider the function: 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?
PROBLEM 4 — APPLIED
You are writing a statistics library. Design a function 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.
PROBLEM 5 — CRITICAL THINKING
Consider the following function: 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.

Lesson 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++.

Varsity Tutors • C++ • Pass-by-Value vs. Pass-by-Reference