Pass By Reference
Reference vs Pointer
References are aliases to existing objects and are often safer than raw pointers for parameter passing:
Feature | Reference | Pointer |
---|---|---|
Null state | Cannot be null in normal use; must bind to a valid object at initialization (can dangle if the object’s lifetime ends) | Can be null; caller/callee must check before use |
Reassignment | Cannot be rebound after initialization | Can be reassigned to point elsewhere |
Syntax | Cleaner: use `.` to access members (no explicit dereference needed) | Use `*` to dereference, `->` to access members |
Safety | Fewer invalid states; expresses “must exist” | More flexible but easier to misuse (dangling/null) |
Optionality | No built-in “no object” value; use pointer or `std::optional | Naturally expresses “maybe none” with `nullptr` |
Move Semantics
C++11 move semantics enable efficient transfer of resources without deep copies. Accept rvalue references (`T&&`) when you intend to *consume* temporaries, and use `std::move` to transfer from lvalues explicitly.
An alternative sink pattern is **pass-by-value then move** inside the function, which handles both lvalues and rvalues with a single overload.
#include <utility>
struct BigData {
BigData() = default;
BigData(const BigData&) = delete; // disable copy
BigData& operator=(const BigData&) = delete;
BigData(BigData&&) noexcept = default; // enable move
BigData& operator=(BigData&&) noexcept = default;
};
// Sink: consumes rvalues (and moved lvalues)
void processBigData(BigData&& data) {
// ... consume 'data' (may std::move into storage) ...
}
// Alternative: pass-by-value then move
void storeBigData(BigData data) {
// 'data' is a local; move it where needed
// storage = std::move(data);
}
BigData createData() { return BigData{}; } // NRVO or move
void demo() {
BigData a;
processBigData(createData()); // binds rvalue
processBigData(std::move(a)); // explicit move from lvalue
BigData b;
storeBigData(createData()); // rvalue path
storeBigData(std::move(b)); // lvalue moved
}
Perfect Forwarding
Forwarding references (aka universal references) preserve the value category of arguments in templates. Use `std::forward
Note: `T&&` is a forwarding reference **only** when `T` is a deduced template parameter; otherwise it’s a plain rvalue reference.
#include <iostream>
#include <string>
#include <utility>
void f(const std::string&) { std::cout << "lvalue\n"; }
void f(std::string&&) { std::cout << "rvalue\n"; }
template <typename T>
void relay(T&& arg) {
// forwards exactly as received
f(std::forward<T>(arg));
}
int main() {
std::string s = "hi";
relay(s); // prints lvalue
relay(std::string{}); // prints rvalue
}
Reference Collapsing Rules
When combining references (e.g., during template deduction), these rules apply:
• `T& &` → `T&`
• `T& &&` → `T&`
• `T&& &` → `T&`
• `T&& &&` → `T&&`
A common idiom: `auto&& x = expr;` yields a forwarding reference that binds to lvalues as `T&` and rvalues as `T&&`.
Common Pitfalls
• **Dangling references**: Binding a reference to an object that goes out of scope causes undefined behavior.
• **Returning references to locals**: Never return a reference to a local variable.
• **Const temporary lifetime**: Binding a `const T&` to a temporary extends the temporary’s lifetime only until the end of the full-expression, not beyond.
// BAD: returns reference to a dead object
const int& bad() {
int x = 42;
return x; // dangling
}
// GOOD: return by value
int good() {
int x = 42;
return x; // copy elision
}
// Subtle: lifetime of a temporary bound to const ref
const std::string& ref = std::string("tmp"); // OK until end of this full-expression
// Do not store 'ref' beyond this statement boundary.
When to Use What (Guidelines)
• Small trivially copyable types (e.g., `int`, `double`): pass **by value**.
• Large read-only inputs: pass **`const T&`**.
• Outputs/mutable inputs: pass **`T&`** (or return by value when possible).
• Optional parameters: use **pointers** (nullable) or `std::optional
• Ownership transfer/sinks: accept **`T&&`** or **by value then move** internally.