20 topics
/
← Back to Quick Reference
Topic 09

Functions

Passing · Overloading · Defaults · Lambdas · std::function · RVO

C++17 · Advanced Reference

Functions in C++

01

Declaration 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. 1.T — by value. Caller's copy unchanged. Use for small cheap types.
  2. 2.const T& — by const ref. No copy, read-only. Use for large objects.
  3. 3.T& — by ref. Modifies caller's variable directly.
  4. 4.T* — by pointer. Optional/nullable. Must null-check inside.
  5. 5.T&& — rvalue ref (move semantics). Transfers ownership of temporaries.

Key rules

  1. 1.Never return a reference or pointer to a local variable — it is destroyed when the function returns.
  2. 2.Return objects by value — the compiler applies RVO/NRVO to eliminate copies.
  3. 3.Prefer const& over pointer for non-optional in-parameters.
  4. 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 valueIndependent 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 pointerLike 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 matchThe compiler prefers the overload whose parameter types exactly match the argument types.
promotionschar and short promote to int; float promotes to double — may select an unexpected overload.
ambiguous callWhen two overloads are equally good, the compiler errors. Resolve with an explicit cast.
return typeReturn 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.
mutableAllows modifying captured-by-copy values inside the lambda body.
auto paramsGeneric 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

06
Storing 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 pointerZero overhead. Only works with free functions and static methods. Syntax is awkward.
std::functionAccepts any callable (pointer, lambda, functor). Small overhead from type erasure. Prefer for APIs.
template paramFastest — 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
RVOUnnamed return value optimization — compiler constructs the return value directly in the caller's storage.
NRVONamed RVO — applies when a single named local variable is returned. Not guaranteed but done by all major compilers.
C++17 elisionReturning a prvalue (temporary) is guaranteed to elide the copy — no move constructor required.
don't std::move returnWriting 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.