C++ Functions: Parameter Passing, RVO, noexcept, Overloading & Defaulted Arguments (C++23)

C++ Functions: Parameter Passing, RVO, noexcept, Overloading & Defaulted Arguments (C++23)
Table of Contents
- Parameter Passing: The Complete Decision Guide
- Pass by Value: When It's Optimal
- Pass by const Reference: The Read-Only Workhorse
- Pass by Rvalue Reference: Sink Parameters
- Return Value Optimization (RVO and NRVO)
- Guaranteed Copy Elision (C++17)
- noexcept: Safety Contracts and Code Generation
- Function Overloading and ADL
- Default Arguments and Their Pitfalls
- std::expected: Error Handling Without Exceptions (C++23)
- Frequently Asked Questions
- Key Takeaway
Parameter Passing: The Complete Decision Guide
Pass by Value: When It's Optimal
For small types (≤ 16 bytes on x86-64 — fits in registers), pass by value is fastest — no indirection, the CPU loads the data directly into registers:
Pass by const Reference: The Read-Only Workhorse
const T& is the workhorse for read-only access to expensive-to-copy types. No copy occurs, and the reference guarantees the function won't modify the data:
[!WARNING] Never return
const T&to a local variable or temporary — the reference becomes dangling the moment the function returns. Return by value instead and let RVO handle the performance.
Pass by Rvalue Reference: Sink Parameters
Rvalue references (T&& as a non-templated function parameter) specifically accept temporaries and std::move'd values — used where the function consumes the argument:
Return Value Optimization (RVO and NRVO)
The C++ myth: "returning a large object from a function is always slow." Wrong. Compilers apply Named Return Value Optimization (NRVO) and the standard mandates Return Value Optimization (RVO):
C++17 Guaranteed Copy Elision: When a function returns a pure prvalue (a temporary expression), the standard mandates the object is constructed directly in the destination — no move, no copy, even if the type has = delete copy/move. This makes:
[!TIP] Never write
return std::move(local_variable);— this explicitly disables NRVO and forces a move where elision would have been free. Justreturn local_variable;.
noexcept: Safety Contracts and Code Generation
noexcept is a contract: this function will never propagate an exception. The compiler uses this to:
- Skip exception handling code generation (smaller binary, faster function epilogues).
- Allow
std::vectorto use move semantics during reallocation — vectors only move elements (O(n) move vs O(n) copy) if the move constructor isnoexcept.
std::expected: Error Handling Without Exceptions (C++23)
std::expected<T, E> (C++23) returns either a value T or an error E — Rust's Result<T, E> in C++:
Frequently Asked Questions
Should I always mark move constructors noexcept?
Yes — always mark move constructors and move assignment operators noexcept if they don't throw (they almost never should). This is one of the most impactful single-character additions you can make to a class. Without it, std::vector and other containers will COPY (not move) your objects during reallocation.
When should I use std::expected vs exceptions vs error codes?
- Exceptions: For truly exceptional conditions (unexpected errors like OOM, disk failure). Recovery is rare.
- std::expected: For expected failure paths that are part of normal control flow (validation, parsing, lookups). Forces callers to handle errors explicitly.
- Error codes: For C API compatibility or performance-critical paths where even
std::expectedoverhead is too much.
Can two functions with the same name but different parameter counts overload? Yes — C++ overloading is based on the number AND types of parameters, NOT on return type. Two functions with identical names but different parameters (or parameter types) are distinct overloads; the compiler selects the best match at the call site via overload resolution.
Key Takeaway
C++ function design is the intersection of performance engineering and API design. The sink parameter idiom (pass by value + move) gives you a single overload that efficiently handles both copies and moves. NRVO/RVO makes returning large objects free. noexcept enables container move semantics and smaller binaries. std::expected provides Rust-style error propagation without exceptions. Combined, these turn function boundaries from performance bottlenecks into zero-cost abstractions.
Read next: C++ Strings & Output: std::string_view, std::format & std::print →
Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineering.
