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.
Aspect | Call by value | Call by reference |
---|---|---|
Definition | Passes 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 syntax | void f(T x) | void f(T& x) (or void f(const T& x) for read-only) |
Effect on caller’s data | No changes propagate back to the caller. | Changes inside the function are visible to the caller. |
Copies made | Yes (may be cheap or expensive depending on T ). | No copy of the object; only an alias is created. |
Performance | Great for small, trivially copyable types (e.g., int , double ). | Efficient for large objects since copying is avoided. |
Memory usage | Extra stack space for the copy. | No extra space for a second object. |
Safety | Safer: function cannot mutate caller’s object accidentally. | Risk of unintended mutation; design APIs to make intent clear. |
Read‑only parameters | Use void f(T x) for small types. | Use void f(const T& x) for large types to avoid copies while preventing mutation. |
When to use | Small, cheap types; when isolation from caller state is desired. | Large objects; when the function must modify the caller’s object or avoid copies. |
Returning results | Prefer 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 intent | Cannot modify caller’s data (mutations affect only the copy). | Use non‑const & for in‑place edits; use const& to promise no edits. |
Nullability | Not applicable; parameters are values. | References are non‑null by design; use pointers (T* ) if “optional”/nullable is needed. |
Overload resolution | Prefers exact value matches; may bind temporaries freely. | Lvalue references bind to lvalues; const& can bind to lvalues and temporaries. |
Temporaries | Passing a temporary copies/moves into the parameter. | const T& safely binds to temporaries; non‑const T& cannot. |
Large containers/objects | Copying 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 effects | No side effects on caller state. | Side effects propagate to the caller’s object (intent must be documented). |
API clarity | Good for pure functions and clear ownership. | Good for in‑place algorithms; pair with clear names/docs indicating mutation. |
Alternatives | For 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. |
Caveats | Unnecessary copies in hot paths can hurt performance. | Avoid returning references to locals; beware iterator/reference invalidation when returning container elements. |
Quick rule of thumb | Small 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)
returnings.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 (orstd::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&
toT
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 whymySwap(const T&, const T&)
cannot compile. - Make
clampToRange
generic for floating-point and test withdouble
.
🔹 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. PreferT&
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.