Understanding Call by Value vs Call by Reference in C++

Call by Value vs Call by Reference in C++ determines whether a function receives a copy of an argument (value) or an alias to the original (reference). Understanding the difference helps you write safer, faster, and more predictable code.

This beginner-friendly guide explains Call by Value vs Call by Reference in C++ with commented examples, outputs, best practices, a clear comparison table, and “Try it yourself” challenges after each section.

🔹 What do “call by value” and “call by reference” mean?

When you pass arguments to a function:

  • Call by value: The function receives a copy. Modifying the parameter does not change the original.
  • Call by reference: The function receives a reference (an alias) to the original object. Modifying the parameter changes the original.
AspectCall by valueCall by reference
DefinitionPasses a copy of the argument to the function (works on its own local copy).Passes an alias to the original object using a reference (works on the original).
Typical syntaxvoid f(T x)void f(T& x) (or void f(const T& x) for read-only)
Effect on caller’s dataNo changes propagate back to the caller.Changes inside the function are visible to the caller.
Copies madeYes (may be cheap or expensive depending on T).No copy of the object; only an alias is created.
PerformanceGreat for small, trivially copyable types (e.g., int, double).Efficient for large objects since copying is avoided.
Memory usageExtra stack space for the copy.No extra space for a second object.
SafetySafer: function cannot mutate caller’s object accidentally.Risk of unintended mutation; design APIs to make intent clear.
Read‑only parametersUse void f(T x) for small types.Use void f(const T& x) for large types to avoid copies while preventing mutation.
When to useSmall, cheap types; when isolation from caller state is desired.Large objects; when the function must modify the caller’s object or avoid copies.
Returning resultsPrefer returning by value (RVO/C++17 elision makes it cheap).Return by non‑const reference only if returning an existing object with a lifetime that outlives the function (avoid dangling references).
Mutability intentCannot modify caller’s data (mutations affect only the copy).Use non‑const & for in‑place edits; use const& to promise no edits.
NullabilityNot applicable; parameters are values.References are non‑null by design; use pointers (T*) if “optional”/nullable is needed.
Overload resolutionPrefers exact value matches; may bind temporaries freely.Lvalue references bind to lvalues; const& can bind to lvalues and temporaries.
TemporariesPassing a temporary copies/moves into the parameter.const T& safely binds to temporaries; non‑const T& cannot.
Large containers/objectsCopying can be expensive; avoid for std::string, std::vector, big structs.Prefer const T& for read‑only access and T& for in‑place modification.
Side effectsNo side effects on caller state.Side effects propagate to the caller’s object (intent must be documented).
API clarityGood for pure functions and clear ownership.Good for in‑place algorithms; pair with clear names/docs indicating mutation.
AlternativesFor very large types, consider pass‑by‑value with move if the caller can provide a temporary.Use pointers for optional parameters (T*) or output parameters; consider returning values instead of using output refs.
CaveatsUnnecessary copies in hot paths can hurt performance.Avoid returning references to locals; beware iterator/reference invalidation when returning container elements.
Quick rule of thumbSmall types: pass by value.Large types: pass by const& for read‑only, by & for in‑place edits.

Try it yourself

  • In one sentence, explain when you’d prefer each style in your current project.
  • List two risks of call by reference and two benefits.

🔹 Basic example: Value vs Reference behavior

Here’s the difference in how updates propagate back to the caller.

#include <iostream>
using namespace std;
void incrementByValue(int x) { // copy, local to function
    x += 1;
}
void incrementByRef(int& x) { // reference, modifies caller's variable
    x += 1;
}
int main() {
    int a = 10, b = 10;
    incrementByValue(a); // a stays 10
    incrementByRef(b);   // b becomes 11
    cout << "a=" << a << ", b=" << b << "\n";
}

Output

a=10, b=11

Copying isolates changes; referencing edits the original. This is the core of Call by Value vs Call by Reference in C++.

Try it yourself

  • Create double d=3.5, pass to value and reference functions, and print after each call.
  • Pass a std::string by value and by reference; append inside the function and observe differences.

🔹 Const Reference: No copy, No modify

Use const T& to avoid copying large objects while preventing mutation. This is ideal for read-only parameters (e.g., const std::string&).

#include <string>
#include <iostream>
using namespace std;
void printTitle(const string& title) { // no copy, cannot modify
    cout << "Title: " << title << "\n";
    // title += "!"; // ERROR: cannot modify const reference
}
int main() {
    string s = "C++ Guide";
    printTitle(s); // efficient and safe
}

Output

Title: C++ Guide

Why use const&?

  • Avoids expensive copies for big objects.
  • Prevents accidental mutation inside the function.
  • Works with temporaries and lvalues.

Try it yourself

  • Write size_t length(const std::string& s) returning s.size(). Pass both a variable and a string literal.
  • Attempt modifying a const& parameter and read the compiler error to learn.

🔹 Passing large objects efficiently

Copying large structs/classes is expensive. Prefer const T& for inputs and T& for outputs/in-place changes.

#include <vector>
#include <iostream>
#include <numeric>
using namespace std;
double average(const vector<int>& v) { // no copy of the vector
    if (v.empty()) return 0.0;
    long long sum = accumulate(v.begin(), v.end(), 0LL);
    return static_cast<double>(sum) / v.size();
}
void scale(vector<int>& v, int k) { // modifies caller's vector
    for (int& x : v) x *= k;
}
int main() {
    vector<int> data{1,2,3,4,5};
    cout << "avg=" << average(data) << "\n";
    scale(data, 3);
    for (int x : data) cout << x << " ";
    cout << "\n";
}

Output

avg=3
3 6 9 12 15

Try it yourself

  • Make average take the vector by value and print timing differences for a large vector.
  • Implement normalize(vector<double>&) that divides each element by the maximum (handle zero safely).

🔹 References vs Pointers (and when to use which)

References are the idiomatic way to express required aliasing. Pointers can be null, reseated, and signal optionality. Prefer references unless you need pointer semantics.

#include <iostream>
using namespace std;
void setRef(int& x, int v) { x = v; } // must bind to an existing int
void setPtr(int* p, int v) { if (p) *p = v; } // may be null
int main() {
    int a = 5, b = 5;
    setRef(a, 42); // always safe (non-null)
    setPtr(&b, 42); // caller must pass a valid pointer
    cout << a << " " << b << "\n";
    int* np = nullptr;
    setPtr(np, 7); // does nothing; checks null
}

Output

42 42

Guideline

  • Use T& for required, non-null arguments.
  • Use T* to express optional, nullable arguments (or std::optional<std::reference_wrapper<T>> in modern code).
  • Never hold references to temporaries that will go out of scope.

Try it yourself

  • Create void maybeSet(int* p, int v) and call it with a null pointer to see safe no-op handling.
  • Rewrite with references and observe the differences in call sites and guarantees.

🔹 Return by value vs Return by reference

Returning by value creates a new object (often optimized by Return Value Optimization). Returning by reference returns an alias to an existing object—only safe if the referred object outlives the call.

#include <string>
#include <iostream>
using namespace std;
string makeGreeting() { // return by value (RVO makes this cheap)
    return "Hello";
}
char& firstChar(string& s) { // return by reference (caller can modify)
    return s[0];
}
int main() {
    string g = makeGreeting(); // cheap in modern C++
    cout << g << "\n";
    firstChar(g) = 'Y'; // modifies g
    cout << g << "\n";
}

Output

Hello
Yello

Beware

  • Never return a reference to a local variable (dangling reference!).
  • Returning by value is often simplest and efficient due to copy elision.

Try it yourself

  • Write a function that mistakenly returns int& bound to a local. Observe the warning/UB, then fix it to return by value.
  • Return a large object by value and inspect the generated code (if comfortable) to see elision.

🔹 Const value vs Const reference parameters

For small, trivially copyable types (int, double), pass by value. For large types (vector, string, custom structs), use const&. Both prevent mutation inside the function when declared const.

  • Pass by value (small types): cheap copy, simpler semantics.
  • Pass by const& (large types): avoids copy, still read-only.

Try it yourself

  • Change a function parameter from const T& to T for a small type and compare assembly size or runtime.
  • Do the same for a large std::vector and measure the difference.

🔹 Advanced: rvalue reference and perfect forwarding (bonus)

To write efficient wrappers, you can forward arguments preserving value category using templates and std::forward. This is beyond basic Call by Value vs Call by reference in C++ but useful to know exists.

#include <utility>
#include <iostream>
using namespace std;
void sink(const string& s) {
    cout << "lvalue path: " << s << "\n";
}
void sink(string&& s) {
    cout << "rvalue path: " << s << "\n";
}
template <typename T>
void wrapper(T&& x) { // forwarding reference
    sink(std::forward<T>(x)); // preserves lvalue/rvalue
}
int main() {
    string a = "C++";
    wrapper(a);              // lvalue path
    wrapper(string("C++"));  // rvalue path
}

Output

lvalue path: C++
rvalue path: C++

Try it yourself

  • Create an emplaceWrapper that forwards constructor arguments to build an object in place.
  • Add prints in overloads to observe resolution with lvalues vs rvalues.

🔹 Mini project: Safe swap and clamp utilities

Let’s implement two simple utilities to cement Call by Value vs Call by Reference in C++ usage: swap (mutates two variables) and clamp (read-only inputs, one mutable output).

#include <iostream>
using namespace std;
template <typename T>
void mySwap(T& a, T& b) { // references: mutate both
    T tmp = a;
    a = b;
    b = tmp;
}
template <typename T>
void clampToRange(const T& lo, const T& hi, T& x) { 
    // const& inputs, & output
    if (x < lo) x = lo;
    else if (x > hi) x = hi;
}
int main() {
    int a = 3, b = 7;
    mySwap(a, b);
    cout << "a=" << a << 
    ", b=" << b << "\n";
    int v = 15;
    clampToRange(0, 10, v);
    cout << "v=" << v << "\n";
}

Output

a=7, b=3
v=10

Try it yourself

  • Add const overloads incorrectly and see why mySwap(const T&, const T&) cannot compile.
  • Make clampToRange generic for floating-point and test with double.

🔹 Best practices and common pitfalls

  • Prefer pass-by-value for small, cheap-to-copy types (int, double, small PODs).
  • Prefer const T& for large read-only inputs. Prefer T& for outputs/in-place edits.
  • Avoid unnecessary copies—watch for pass-by-value in tight loops or with large objects.
  • Do not return references to locals (dangling). Be careful returning references to container elements that may be invalidated.
  • Use references to express required non-null aliasing; use pointers to express optionality.
  • Document mutation: if a function changes parameters, make it obvious via non-const references or return values.

Try it yourself

  • Audit one module: convert large pass-by-value parameters to const& and measure improvements.
  • Find a function that mutates state implicitly and refactor to use explicit & parameters or return values.

🔹 FAQs about Call by Value vs Call by Reference in C++

Q1: Is pass-by-reference always faster?
Not always. For small trivially copyable types, pass-by-value can be as fast or faster and simplifies ownership.

Q2: When should I use const T&?
When the object is large or expensive to copy and you only need to read it.

Q3: Can I bind a reference to a temporary?
A non-const lvalue reference cannot. A const T& can bind to temporaries, extending their lifetime for the reference’s scope.

Q4: Are references nullable?
No. References must refer to valid objects. Use pointers to represent optional/null.

Q5: Does pass-by-value copy every time?
Yes, unless optimized away (e.g., small types may be passed in registers). For objects, copying invokes their copy/move constructors as needed.

Q6: Should output parameters be references or return values?
Prefer return values for single outputs (clean and composable). Use references for multiple outputs or in-place edits of large objects.

Q7: What about const T by value?
const on a by-value parameter only applies inside the function; the caller’s object is unaffected, and a copy still occurs.

Q8: How do I avoid dangling references?
Never return references to locals; avoid storing references to objects that may go out of scope or be moved.

Q9: Do move semantics change this choice?
They complement it: pass by value can be efficient when callers pass temporaries (moved into the function). Otherwise, use const& for large read-only objects.

Q10: References vs pointers—best default?
Prefer references for required, non-null aliases. Use pointers for optional/nullable parameters.

🔹 Wrapping up

Call by Value vs Call by reference in C++ is a trade-off between isolation and efficiency. Use value for small, cheap types and when you want to protect the caller’s data. Use references for large objects, read-only const& for performance, and non-const & for intentional mutation. Practice the “Try it yourself” tasks to make the right choice instinctive in your code.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.