Pass By Value Vs Pass By Reference C++

9 min read

Understanding how data moves between functions is a foundational concept that separates novice C++ programmers from seasoned engineers. In C++, the mechanism used to pass arguments—pass by value versus pass by reference—dictates memory usage, performance characteristics, and whether the original variable can be modified. Mastering this distinction is essential for writing efficient, bug-free code, especially when working with large objects or performance-critical systems.

What Is Pass by Value?

Pass by value is the default parameter passing mechanism in C++. When a function parameter is defined without the reference symbol (&), the compiler creates a copy of the argument passed by the caller. This copy exists as a distinct variable in the function’s stack frame, completely independent of the original variable Most people skip this — try not to..

How It Works Under the Hood

When you call a function using pass by value, the following sequence occurs:

  1. The compiler allocates memory for the parameter on the stack.
  2. The copy constructor of the argument's type is invoked to initialize this new parameter with the value of the argument.
  3. The function executes using this local copy.
  4. Upon returning, the parameter's destructor is called, and the stack memory is reclaimed.

Code Example

#include 

void modifyValue(int num) {
    num = 20; // Modifies the local copy only
    std::cout << "Inside function: " << num << std::endl;
}

int main() {
    int value = 10;
    std::cout << "Before call: " << value << std::endl; // Output: 10
    modifyValue(value);
    std::cout << "After call: " << value << std::endl;  // Output: 10 (Unchanged)
    return 0;
}

In this snippet, value remains 10 because modifyValue operates on a clone.

Advantages and Disadvantages

Pros:

  • Safety & Immutability: The original data is protected from unintended side effects. This aligns with functional programming principles where functions are pure and predictable.
  • Simplicity: No syntax modifiers are required; it is the default behavior.
  • Thread Safety: Since each thread gets its own copy when passing by value, there are no race conditions on the parameter itself (assuming the copied data doesn't contain shared pointers or references).

Cons:

  • Performance Overhead: For large objects (like std::vector, std::string, or custom classes), invoking the copy constructor involves deep memory allocation and data duplication. This is expensive in terms of CPU cycles and memory bandwidth.
  • Slicing Problem: Passing a derived class object by value to a function expecting a base class parameter slices off the derived portion, leading to polymorphic behavior loss.

What Is Pass by Reference?

Pass by reference allows the function to operate directly on the original variable. Instead of creating a copy, the parameter becomes an alias—an alternative name—for the argument. This is achieved by appending the & symbol to the parameter type in the function signature.

How It Works Under the Hood

Mechanically, references are typically implemented by the compiler as pointers (memory addresses) that are automatically dereferenced. Still, syntactically, they behave exactly like the original variable Worth knowing..

  1. No new memory is allocated for the object itself (only potentially for the reference address, often optimized into a register).
  2. No copy constructor is called.
  3. Any operation on the parameter affects the original argument immediately.

Code Example

#include 

void modifyReference(int& num) {
    num = 20; // Modifies the actual argument
    std::cout << "Inside function: " << num << std::endl;
}

int main() {
    int value = 10;
    std::cout << "Before call: " << value << std::endl; // Output: 10
    modifyReference(value);
    std::cout << "After call: " << value << std::endl;  // Output: 20 (Changed!)
    return 0;
}

Here, value changes to 20 because num is just another name for value Worth keeping that in mind..

Lvalue References vs. Rvalue References (C++11 and Beyond)

Modern C++ distinguishes between two types of references:

  • Lvalue References (Type&): Bind to named, persistent objects (variables). Day to day, used for standard pass-by-reference. * Rvalue References (Type&&): Bind to temporary objects (rvalues) or objects explicitly cast via std::move. This enables move semantics, allowing resources to be "stolen" from temporaries rather than copied, revolutionizing performance for heavy objects.

The Critical Role of const Correctness

A major pitfall of pass by reference is the loss of const-safety. Still, if a function takes a non-const reference (void func(int&)), the caller must assume the variable will be modified. This makes code harder to reason about Surprisingly effective..

Best Practice: If the function does not need to modify the argument, always pass by const reference (const Type&).

// Efficient read-only access
void printLargeObject(const std::vector& vec) {
    // vec.push_back(10); // Compile Error: Cannot modify const reference
    for (int n : vec) std::cout << n << " ";
}

This gives you the performance benefit of no-copy with the safety guarantee of pass-by-value.

Performance Deep Dive: When to Use Which?

The decision matrix depends heavily on the size and copy cost of the type, and whether the function needs to modify the argument.

1. Small, Trivially Copyable Types (int, double, char, pointers, small structs)

Recommendation: Pass by Value.

  • Reasoning: The overhead of setting up a reference (passing a pointer address, dereferencing inside the function) is often equal to or greater than copying a few bytes (e.g., 4 or 8 bytes) directly into a register. Modern compilers optimize pass-by-value for these types aggressively.

2. Large Objects or Expensive-to-Copy Types (std::string, std::vector, std::map, custom classes with dynamic memory)

Recommendation: Pass by Const Reference (const Type&) for read-only; Pass by Reference (Type&) for modification.

  • Reasoning: Avoids deep copy allocation/deallocation. The cost of an indirect memory access (dereferencing the reference) is negligible compared to heap allocation.

3. Types Designed for Move Semantics (Standard Library Containers, Smart Pointers)

Recommendation: Pass by Value (Sink Arguments) + std::move.

  • Pattern: If a function consumes the argument (takes ownership), pass by value. The caller uses std::move to cast the argument to an rvalue. The parameter is then move-constructed (cheap pointer swap), and the function can move it again into a member variable.
  • Example: void setName(std::string name) { this->name = std::move(name); }

4. Polymorphic Base Classes

Recommendation: Pass by Reference (or Pointer).

  • Reasoning: Pass by value causes object slicing. The derived part of the object is sliced off, leaving only the base class portion. Virtual function calls will resolve to the base class implementation, breaking polymorphism.

Common Pitfalls and How to Avoid Them

Dangling References

Returning a reference to a local variable created inside a function is undefined behavior. The local variable is destroyed when the function returns, leaving the reference pointing to invalid stack memory.


### Dangling References (Continued)
Returning a reference to a local variable created inside a function is undefined behavior. The local variable is destroyed when the function returns, leaving the reference pointing to invalid stack memory.

```cpp
// BAD: Returns reference to local variable
int& badFunction() {
    int local = 42;
    return local; // local is destroyed here
}

// GOOD: Return by value
int goodFunction() {
    int local = 42;
    return local;
}

Binding References to Temporaries

Non-const references cannot bind to temporary objects (rvalues), which prevents accidental modification of short-lived objects. That said, const references can bind to temporaries, extending their lifetime only within the scope of the function call. Be cautious when storing references to temporaries in longer-lived containers Took long enough..

void process(const std::string&); // Accepts both lvalues and rvalues

void badExample() {
    const std::string& ref = "temporary"; // OK: lifetime extended to end of scope
    // ref is valid until the end of badExample()
}

// ERROR: Non-const reference cannot bind to rvalue
void badRef(std::string& s);
badRef("hello"); // Compilation error
``

### Reference Lifetime in Containers  
Storing a plain reference inside a container is unsafe because the container may outlive the object to which the reference refers. When the underlying object is destroyed, the reference becomes dangling, leading to undefined behavior the next time the container is accessed.

```cpp
std::vector badStore;   // ❌ dangerous
std::string s = "hello";
badStore.push_back(s);                // s is stored as a reference
// … later, s goes out of scope or is moved from …
std::string& r = badStore[0];         // undefined behavior if s is no longer valid

Fix: Use a container of smart pointers or references (std::vector<std::unique_ptr<T>>, std::vector<T*>) that manage the lifetime of the pointed‑to objects. If ownership is not required, store std::reference_wrapper<T> which explicitly indicates that the wrapped object must stay alive That alone is useful..

std::vector> safeStore;
safeStore.emplace_back(s);   // stores a wrapper, not a raw reference

Aliasing and Ownership

Multiple references or pointers can alias the same object, which is useful but requires careful consideration of mutability. If one alias mutates the object, all aliases see the change. This can hide bugs when the intended semantics are “read‑only” or “exclusive”.

int value = 10;
int& r1 = value;
int& r2 = value;

r1 = 20;               // r2 now also yields 20

To avoid unintended aliasing, prefer std::unique_ptr for exclusive ownership or make copies when the function’s contract requires independent values Small thing, real impact..

Const Correctness with References

A const reference guarantees that the function will not modify the bound object, enabling the compiler to catch accidental writes and allowing the argument to be bound to temporaries.

void printLength(const std::string& s) {
    std::cout << s.size() << '\n';
}
printLength("temporary");   // OK: const lvalue reference binds to a temporary

If a non‑const reference is used, the function must promise not to modify the argument, which is error‑prone. In generic code, const T& is the default unless mutation is explicitly required.

Using std::move with References

std::move does not change the value itself; it merely casts an lvalue to an rvalue reference. This enables overload resolution to select move‑only functions.

std::string createString() {
    return std::string("dynamic");
}

void consume(std::string&& s) {
    // s is now an rvalue reference; we can move from it
    std::cout << std::move(s) << '\n';
}

int main() {
    std::string str = createString();   // str is an lvalue
    consume(std::move(str));            // str is cast to rvalue, move happens
}

Be aware that after std::move, the original object is left in a valid but unspecified state; for std::string it will be empty, for other types it may be undefined.

Best‑Practice Checklist

  • Pass by value when the function takes ownership of the argument; use std::move in the caller.
  • Pass by (const) reference when the function only reads or observes the argument; avoid slicing for polymorphic types.
  • Never return a reference to a local variable; return by value or use a reference that outlives the referenced object.
  • Prefer smart pointers or std::reference_wrapper for storing references in containers.
  • Mind aliasing: use exclusive ownership (unique_ptr) when mutation must be unique, otherwise document the shared‑ownership intent.
  • Apply const qualifiers liberally to const references to convey intent and enable binding to temporaries.
  • put to work move semantics (std::move, rvalue references) to eliminate unnecessary copies, especially for large objects.

Conclusion

References are a powerful tool for expressing efficient, readable C++ code, but they carry hidden lifetime responsibilities. By passing by reference (or pointer) for read‑only or polymorphic parameters, using value parameters combined with std::move when ownership is transferred, and carefully managing the lifetime of objects that references alias, developers can avoid the classic pitfalls of dangling references, slicing, and unintended aliasing. Following the checklist above ensures that reference usage remains both safe and performant, leading to dependable, maintainable software Worth keeping that in mind. That alone is useful..

Just Finished

New Arrivals

A Natural Continuation

Cut from the Same Cloth

Thank you for reading about Pass By Value Vs Pass By Reference C++. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home