20 topics
← Back to Quick Reference/
Topic 09
FFuunnccttiioonnss
Passing · Overloading · Defaults · Lambdas · std::function · RVO
C++17 · Advanced ReferenceFunctions in C++
01Declaration vs Definition
A declaration tells the compiler a function exists and what its signature is. A definition provides the body. You can declare many times but define exactly once (ODR). Declarations live in headers; definitions in .cpp files.
Parameter passing summary
- 1.
T— by value. Caller's copy unchanged. Use for small cheap types. - 2.
const T&— by const ref. No copy, read-only. Use for large objects. - 3.
T&— by ref. Modifies caller's variable directly. - 4.
T*— by pointer. Optional/nullable. Must null-check inside. - 5.
T&&— rvalue ref (move semantics). Transfers ownership of temporaries.
Key rules
- 1.Never return a reference or pointer to a local variable — it is destroyed when the function returns.
- 2.Return objects by value — the compiler applies RVO/NRVO to eliminate copies.
- 3.Prefer
const&over pointer for non-optional in-parameters. - 4.Mark functions that don't modify the object
const(member functions).
C++17 guarantees copy elision for prvalue returns — returning a temporary is always zero-cost.
Parameter Passing
02// ── Pass by value — caller's copy is unchanged ─────────────── void doubleIt(int n) { n *= 2; } // modifies local copy only int x = 5; doubleIt(x); // x is still 5 // ── Pass by reference — modifies the original ──────────────── void doubleIt(int& n) { n *= 2; } doubleIt(x); // x is now 10 // ── Pass by const reference — read-only, no copy ───────────── void print(const std::string& s) { std::cout << s; } // ✅ No copy of the string — fast even for large objects // ── Pass by pointer — optional / nullable reference ────────── void maybeDouble(int* p) { if (p) *p *= 2; // caller passes nullptr to skip } maybeDouble(&x); // ✅ maybeDouble(nullptr); // ✅ safe — null check inside // ── Rule of thumb ──────────────────────────────────────────── // Small/cheap types (int, double, char): pass by value // Large objects, read-only: pass by const& // Must modify caller's variable: pass by & // Optional / nullable: pass by pointer
| by value | Independent copy — changes inside the function don't affect the caller. Best for int, double, char, small structs. |
| by const& | Read-only alias — no copy. Best for std::string, vectors, any large object you won't modify. |
| by & | Mutable alias — directly modifies the caller's variable. Use when the function's purpose is to change the argument. |
| by pointer | Like reference but nullable. Use when passing nullptr is a meaningful option. |
Function Overloading
03// Overloading — same name, different parameter types/count int area(int side) { return side * side; } double area(double side) { return side * side; } int area(int w, int h) { return w * h; } area(5); // calls area(int) area(3.0); // calls area(double) area(4, 6); // calls area(int, int) // ── Resolution rules (simplified) ─────────────────────────── // 1. Exact match → preferred // 2. Trivial conversions → const, array→pointer // 3. Promotions → char→int, float→double // 4. Standard conversions → int→double, derived→base // 5. User-defined conversions // 6. Variadic (...) // ── Overloading pitfalls ───────────────────────────────────── void foo(int); void foo(double); foo(3.14f); // ⚠ ambiguous: float → int or float → double? // ── Cannot overload on return type alone ───────────────────── // int get(); ❌ same name + same params = redefinition // bool get();
| exact match | The compiler prefers the overload whose parameter types exactly match the argument types. |
| promotions | char and short promote to int; float promotes to double — may select an unexpected overload. |
| ambiguous call | When two overloads are equally good, the compiler errors. Resolve with an explicit cast. |
| return type | Return type is NOT part of the overload signature — you cannot overload on return type alone. |
Default Arguments & Templates
04// Default arguments — must be rightmost parameters void connect(std::string host, int port = 80, bool ssl = false); connect("example.com"); // port=80, ssl=false connect("example.com", 443); // ssl=false connect("example.com", 443, true); // ── Rules ──────────────────────────────────────────────────── // Defaults are set in the declaration (usually the header) // Once a parameter has a default, all to its right must also // Defaults can reference earlier parameters? No — not allowed // void bad(int a, int b = a) {} // ❌ not allowed // ── Default vs overload ────────────────────────────────────── // Defaults are syntactic sugar — caller still passes the value // Overloads can have completely different implementations // Use defaults when the logic is identical; overloads otherwise // ── Template default arguments ─────────────────────────────── template<typename T = double> T zero() { return T{}; } zero(); // returns 0.0 (double) zero<int>(); // returns 0
Defaults go in the declaration, not the definition. If you split declaration (header) and definition (.cpp), put the default values only in the header — the compiler reads the declaration at the call site.
Lambda Expressions (C++11/14)
05// Lambda syntax: [capture](params) -> return_type { body } auto square = [](int x) { return x * x; }; square(5); // 25 // ── Captures ───────────────────────────────────────────────── int factor = 3; auto mul = [factor](int x) { return x * factor; }; // copy auto inc = [&factor](int x) { factor++; return x; }; // ref // [=] capture all locals by copy // [&] capture all locals by reference // [=, &x] all by copy except x by ref // [this] capture the current object (member access) // ── Common uses ─────────────────────────────────────────────── std::vector<int> v = {3, 1, 4, 1, 5}; std::sort(v.begin(), v.end(), [](int a, int b){ return a > b; }); std::for_each(v.begin(), v.end(), [](int x){ std::cout << x; }); // ── Mutable lambda (modify captured copies) ────────────────── auto counter = [n = 0]() mutable { return ++n; }; counter(); // 1 counter(); // 2 // ── Generic lambda (C++14) ─────────────────────────────────── auto println = [](const auto& x) { std::cout << x << "\n"; }; println(42); println("hi"); println(3.14);
| [=] | Capture all locals by copy. Safe but may copy large objects — be specific when it matters. |
| [&] | Capture all by reference. Fast, but the lambda must not outlive the captured variables. |
| mutable | Allows modifying captured-by-copy values inside the lambda body. |
| auto params | Generic lambda (C++14) — auto parameters make it a template, callable with any type. |
Prefer specific captures over [=] or [&]. Naming what you capture makes dependencies explicit and prevents accidentally capturing
this or large objects.Function Pointers & std::function
06Storing and passing callables
// ── Function pointer ───────────────────────────────────────── int add(int a, int b) { return a + b; } int (*op)(int, int) = add; // declare + assign op(3, 4); // 7 // Using typedef / using for readability using BinOp = int(*)(int, int); BinOp fn = add; // Array of function pointers (dispatch table) BinOp ops[] = { add, subtract, multiply }; ops[0](3, 4); // calls add // ── std::function — type-erased callable ───────────────────── #include <functional> std::function<int(int,int)> f = add; // function pointer f = [](int a, int b){ return a + b; }; // lambda f = std::bind(add, std::placeholders::_1, 10); // partial apply // ── When to use which ──────────────────────────────────────── // function pointer — zero overhead, only free functions/static methods // std::function — flexible, accepts anything callable, ~small heap alloc // template param — fastest (inlined), but increases compile time + code size template<typename F> void apply(F fn, int x) { fn(x); }
| function pointer | Zero overhead. Only works with free functions and static methods. Syntax is awkward. |
| std::function | Accepts any callable (pointer, lambda, functor). Small overhead from type erasure. Prefer for APIs. |
| template param | Fastest — inlined by compiler. Use when callable type is known at compile time (e.g. sort comparator). |
Return Value Optimization (RVO)
07// Return Value Optimization — compiler eliminates the copy std::vector<int> makeVec() { std::vector<int> v = {1, 2, 3, 4, 5}; return v; // RVO: constructed directly in caller's storage } auto data = makeVec(); // no copy, no move — zero cost return // ── Named RVO (NRVO) ───────────────────────────────────────── std::string buildMsg(bool err) { std::string msg; // named local variable if (err) msg = "error"; else msg = "ok"; return msg; // NRVO applies when one path returns same var } // ── Guaranteed copy elision (C++17) ────────────────────────── // Returning a prvalue (temporary) is GUARANTEED to elide the copy // The object is constructed directly at the call site Widget makeWidget() { return Widget{args}; } // zero copies, guaranteed // ── Practical rule ─────────────────────────────────────────── // Return objects by value — trust RVO/NRVO // Don't return local variables by reference — dangling reference! // int& bad() { int x = 5; return x; } // ❌ UB — x destroyed on return
| RVO | Unnamed return value optimization — compiler constructs the return value directly in the caller's storage. |
| NRVO | Named RVO — applies when a single named local variable is returned. Not guaranteed but done by all major compilers. |
| C++17 elision | Returning a prvalue (temporary) is guaranteed to elide the copy — no move constructor required. |
| don't std::move return | Writing return std::move(x) disables NRVO — let the compiler do it. |
Return by value freely. With RVO, NRVO, and C++17 guaranteed elision, returning even large objects by value is typically free. Never return a local by reference.