Functions — the unit of structure and the contract between TUs
Functions in C++ are not "named blocks of code" — they are the basic unit of contract between translation units. How a function is declared, how it accepts arguments, how it returns, and what guarantees it makes (noexcept, [[nodiscard]]) decides the library's interface, the optimizer's behavior, and the set of errors you can catch at build time.
Many subtle C++ bugs — dangling references, surprise copies, ODR violations, ambiguous overloads — are not bugs in the function body. They are bugs in the signature. So it pays to think of a function as a structure with five independent dimensions: where it is declared and defined, how it takes input, how it returns, what guarantees it makes, and how it can be passed around as a value.
C++ has layered extra entities on top of functions over time — functors (classes with operator()), lambdas (anonymous functions with captured context), std::function (a type-erased wrapper). They all obey the same signature and overload rules but offer different trade-offs between flexibility and cost. The full map lives in the layers below.
Topic map
- Declarations, definitions, and ODR — declaration vs definition, ODR,
inline, default arguments, what may live in a header. - Parameters — value, ref, const&, move — four ways to pass, move semantics, forwarding reference, ref-qualifiers on member functions.
- Overloading and overload resolution — what's part of the signature, candidate ranking, ambiguity, name hiding, SFINAE/concepts.
- Return values, noexcept, and the stack — RVO/NRVO, structured bindings,
noexcept,[[nodiscard]], recursion, stack frame. - Functors — classes with
operator(),std::function,std::bind, the cost of type erasure. - Lambda expressions — captures, generic lambdas, mutable,
constexprlambdas, IIFE, recursive lambdas.
Common traps
| Mistake | Consequence |
|---|---|
Defining a non-inline function in a header | multiple definition at link time |
return std::move(local); | Inhibits NRVO — copy/move instead of elision |
Passing a large object by value where const& would do | One needless copy on a hot path |
Using T* where a non-null reference belongs | Loses the "not nullptr" guarantee |
Treating T&& in a template as always rvalue | It is a forwarding reference — binds to lvalues too |
| Changing a default argument in a header | Silent ABI break — clients see different defaults |
| Two overloads that differ only in return type | Compile error — return type is not part of the signature |
Hiding inherited overloads without using Base::f | d.f(1) doesn't compile even though Base::f(int) exists |
noexcept on a function that can throw | std::terminate on the first thrown exception |
| Deep recursion with no tail-call guarantee | Stack overflow in release; diverging debug/release behavior |
Interview relevance
Functions on an interview are a C++ semantics test, not a syntax test. A candidate who confuses declaration with definition, or thinks inline is about optimization, usually does not pass a middle-level loop.
Typical checks:
- The difference between declaration and definition, ODR, why
inlinebelongs on a header function. - When to use
const&, when to passTby value, when to useT&&. - What a forwarding reference is and how it differs from an rvalue reference.
- What happens with
std::vector::push_backif the element's move constructor is notnoexcept. - How
f(1.0f)resolves whenf(int)andf(double)both exist. - What RVO/NRVO are and why
return std::move(local)is an anti-pattern. - How
std::functiondiffers from a raw function pointer in performance.
Common wrong answer: "inline makes the function faster by forcing the compiler to inline the body." That is exactly the opening to explain that modern optimizers ignore inline as a hint, and the keyword's real purpose is to allow multiple definitions across TUs.