Chapter 7: Functions and Callable Things
Functions in C++ are the gateways to behavior. They accept parameters, return values, and may carry defaults. You can overload them, ask the compiler to evaluate them at compile time, and bundle behavior in objects and lambdas. This chapter defines the building blocks, the rules that guide overload resolution, and the techniques for efficient argument passing and perfect forwarding.
Parameters, return types, and default arguments
Every function has a signature: a name, parameter types, and a return type. Defaults allow callers to omit trailing arguments. Prefer clear parameter types and names; keep default arguments stable once published, because they participate at the call site during overload resolution.
Declaring parameters and return types
Use references for large objects you do not want to copy. Use const where mutation is not intended. Return by value for small or move friendly types. Trailing return types can improve readability with complex types.
int add(int a, int b);
std::string join(const std::string& a, const std::string& b);
auto make_pair_sum(int a, int b) -> std::pair<int,int> {
return {a + b, a - b};
}
Default arguments
Default arguments fill in when the caller omits trailing values. Defaults are looked up at the call site. Avoid setting defaults in multiple declarations; place them in a single header declaration and keep the definition free of defaults.
void log(const std::string& msg, int level = 1);
void use() {
log("hello"); // level = 1
log("warn", 2); // explicit
}
Return value optimization and noexcept
Modern compilers elide copies in common cases. Mark functions noexcept when they cannot throw; this enables stronger optimizations and cleaner code when composing operations.
std::string greet() noexcept {
return "hi";
}
Overloading and overload resolution
C++ allows multiple functions with the same name as long as their parameter types differ. The compiler selects the best match by ranking viable candidates. Write overload sets that are unambiguous and easy to predict.
Creating a coherent overload set
Keep behavior consistent across overloads. Avoid surprising implicit conversions. Prefer distinct parameter types or tag types to separate meanings that would otherwise collide.
void draw(int radius);
void draw(double radius);
void draw(std::string_view name);
How resolution ranks candidates
Resolution prefers exact matches, then promotions, then standard conversions, then user defined conversions. Templates add another layer: partial ordering among templates decides which template is more specialized.
| Rank | Examples | Comment |
| Exact match | int to int | No conversion |
| Promotion | char to int | Integral promotions |
| Standard conv. | int to double | Widening conversions |
| User defined | Foo to int | operator int() |
SFINAE and constraints preview
SFINAE (Substitution Failure Is Not An Error) is the rule that lets templates quietly step aside when a substitution fails. The compiler tries to form a valid function signature by plugging in template arguments; if that attempt produces an invalid type or expression inside the immediate context, the candidate is simply removed from the overload set rather than causing an error. This graceful collapse lets you write families of templates that become available only when their requirements make sense, which creates flexible and predictable interfaces.
Templates can be conditionally viable using traits or concepts. With constraints, overloads become readable, and resolution becomes simpler to reason about.
template<typename T>
requires std::integral<T>
T sum(T a, T b) { return a + b; }
Using inline, constexpr, and consteval
These specifiers guide linkage and evaluation time. inline affects the one definition rule and allows multiple definitions across translation units with identical bodies. constexpr enables compile time evaluation when arguments are constant expressions. consteval requires compile time evaluation.
Using inline
Use inline on functions defined in headers to allow a single logical definition across all translation units. It does not force inlining by the optimizer; it controls linkage and the one definition rule.
inline int square(int x) { return x * x; }
Using constexpr
constexpr functions may run at compile time or runtime. Write them with a body that also makes sense at runtime. Avoid heavy dynamic work; rely on pure calculations and simple data.
constexpr int fib(int n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
static_assert(fib(5) == 5);
Using consteval
consteval enforces compile time evaluation. Use it for utilities that must produce constant values such as fixed lookups or validated constants.
consteval int id() { return 42; }
constexpr int k = id(); // ok, must be constant
consteval function cannot be called at runtime; every call must appear in a constant evaluation context.
Lambdas and captures
Lambdas are compact function objects. They can capture variables from the surrounding scope by value or by reference. Use clear capture lists to show intent and to manage lifetime safely.
Basic lambda syntax
The form is [captures](params) specifiers { body }. Default captures exist for convenience, yet an explicit list improves clarity and prevents accidental references.
int factor = 3;
auto times = [factor](int x) { return x * factor; };
std::cout << times(7);
Capturing by value vs reference
By value copies the variable at creation time. By reference binds to the original variable and requires that the captured object outlives the lambda. Prefer value for safety unless mutation of the original is required.
int n = 0;
auto by_val = [n]() { return n; };
auto by_ref = [&n]() { ++n; return n; };
Mutable lambdas and auto params
A mutable lambda allows modification of its captured-by-value state. With auto in parameters, a lambda becomes a generic callable that deduces argument types.
auto counter = [n = 0]() mutable { return ++n; };
auto apply = [](auto f, auto x) { return f(x); };
[ptr = std::move(p)]{…}. This avoids shared state and clarifies ownership.
std::function, function pointers, and callable objects
C++ treats many things as callable: free functions, member functions, lambdas, and objects with operator(). std::function is a type erased wrapper that can hold any callable with a given signature.
Function pointers
Function pointers store the address of a free function or a static member function. They are light and fast, yet less flexible than std::function or templates.
int twice(int x) { return 2 * x; }
int (*fp)(int) = &twice;
std::cout << fp(21);
std::function as a general holder
std::function<R(Args…)> can store lambdas, function pointers, and function objects. It allocates when the target does not fit small buffer optimization, so use it for type erasure at interfaces and prefer templates for inner loops.
#include <functional>
std::function<int(int)> f = [](int x){ return x + 1; };
f = twice;
std::cout << f(10);
Callable objects with operator()
Overloading operator() creates a function object with state. This pattern pairs data and behavior, and it composes well with algorithms.
struct scaler {
double k;
int operator()(int x) const { return int(k * x); }
};
scaler s{2.5};
std::cout << s(4);
std::function, be aware of allocations; pass heavy callables by reference when possible.
Argument passing and perfect forwarding
Choose parameter forms that match usage: pass small types by value, large read-only types by const&, and move-only or sink parameters by rvalue reference. Templates can forward arguments perfectly so that value categories are preserved.
Value, reference, and rvalue reference
Pick the simplest approach that avoids unnecessary copies. Value works well for small types. const& avoids copies for read-only access. Rvalue references accept temporaries that can be moved from.
void take_by_value(std::string s);
void take_by_cref(const std::string& s);
void take_by_rref(std::string&& s);
Forwarding references
A parameter of the form T&& in a deduced context becomes a forwarding reference. Use std::forward<T> to preserve lvalue or rvalue nature when passing along.
template<typename T, typename F>
void call(F&& f, T&& x) {
std::forward<F>(f)(std::forward<T>(x));
}
Perfect forwarding in factories
Constructors often need to accept many argument shapes. Forwarding lets a factory pass them straight through to the constructed object with minimal overhead.
template<typename T, typename... Args>
std::unique_ptr<T> make_u(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
std::make_unique and std::make_shared in real code; they are exception safe and concise.