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.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 7 figures

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

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.

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.

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.

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.

PASS-BY-VALUEPASS-BY-REFERENCECaller's Stack Frame (main)x= 5addr: 0x7ff0Callee's Stack Frame — increment(int n)n= 5 (copy)addr: 0x7fa0copy valuen++ → n = 6x still = 5 ✓Caller's Stack Frame (main)x= 5addr: 0x7ff0Callee's Stack Frame — increment(int& n)nalias → 0x7ff0alias (same memory)n++ → x = 6x changes to 6 !
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.

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)

Tcopy(n)=O(sizeof(T))fortrivialtypes,O(n)forcontainersofnelementsT_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

Tref=O(1)=sizeof(pointer)=8byteson64−bitarchitecturesT_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.

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

Parameter Passing Decision FlowchartFunction receives argumentNeed to modify caller's data?YESvoid f(T& param)Pass by referenceNOIs sizeof(T) ≤ 2 × sizeof(void*)?YES (small/cheap)void f(T param)Pass by valueNO (large)void f(const T& param)Pass by const referenceRule of thumb: "cheap to copy" ≈ sizeof(T) ≤ 16 bytesint, double, char, pointers, small structs → value; string, vector, map → const ref
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&.
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)

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

    Step 3 — Correct Implementation with References

    Now 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. 4

    Step 4 — Generalize with Templates

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

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.

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

Comparative analysis of C++ parameter-passing strategies

⚙

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.

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.

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

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

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.
Show answer
After the call, x = 5 and y = 15. The parameter 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

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);
Show answer
The function is: 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

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?
Show answer
The parameter 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

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.
Show answer
A well-designed signature would be: 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

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.
Show answer
After the call, x = 2. Both 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++.

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