Pass By Reference
Lesson outline
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.
Perfect Forwarding
Forwarding references (aka universal references) preserve the value category of arguments in templates. Use std::forward to pass lvalues as lvalues and rvalues as rvalues.
Note: T&& is a forwarding reference only when T is a deduced template parameter; otherwise it's a plain rvalue reference.
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.
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; references have no null state. - Ownership transfer/sinks: accept
T&&or by value then move internally.