Functions
Functions in C++ extend C’s substantially. The additions are default arguments, function overloading, function templates (treated separately in Templates), lambda expressions, member functions of classes (with const-qualification, &/&& qualification, and the virtual mechanism), and type-erased callables via std::function. The language’s function model is correspondingly large; reading C++ code requires fluency with the conventions for each form. This page covers free functions, default arguments, overloading, variadic functions, lambdas, function objects, std::function, and the function specifiers (inline, constexpr, consteval, [[noreturn]], [[nodiscard]]).
Definitions and prototypes
A function definition gives the function a body; a declaration (a prototype) gives the function a type without a body:
int square(int x); // declaration
int square(int x) { return x * x; } // definition
int pair_sum(std::pair<int, int> p); // declaration in a header
int pair_sum(std::pair<int, int> p) { // definition in a source file
return p.first + p.second;
}
Prototypes appear in headers; definitions appear in source files. The compiler does not verify that prototype and definition agree across translation units beyond their token-level equivalence required by the One Definition Rule; the conventional defence is to include the same header in both the calling and the defining translation units.
Parameters and return
Parameters are passed by value by default — the function receives a copy:
void increment(int n) {
n = n + 1; // affects only the local copy
}
int x = 5;
increment(x);
// x is still 5
To modify a caller’s variable, take the parameter by reference:
void increment(int &n) { n = n + 1; }
int x = 5;
increment(x);
// x is now 6
To accept any value (including temporaries) without copying, take by const reference:
void print(const std::string &s);
print("hello"); // binds to a temporary string
print(std::string{"world"}); // binds to an rvalue
print(my_string); // binds to an lvalue
To take ownership of a movable parameter, take by value (and std::move into the destination if needed):
class Document {
public:
Document(std::string title) // by value: caller chooses
: title_(std::move(title)) // move into the member
{}
private:
std::string title_;
};
The conventional parameter-passing choices in modern C++:
| Need | Take as |
|---|---|
| Read-only access to a value | const T& (or T for small types) |
| Modify the caller’s value | T& |
| Take ownership of a movable | T (by value) |
| Optional access | const T* (raw pointer; nullable) |
| Read-only string | std::string_view |
| Read-only contiguous range | std::span<const T> (C++20) |
The return type appears before the function name (or in trailing position with auto … -> T). Return-by-value is the default and is essentially free for most types thanks to RVO and copy elision (treated in Move semantics).
Default arguments
C++ admits default arguments: parameters with a default value used when the caller omits the argument:
void connect(const std::string &host, int port = 80, int timeout_ms = 5000);
connect("example.com"); // port=80, timeout=5000
connect("example.com", 443); // port=443, timeout=5000
connect("example.com", 443, 10000); // all explicit
Defaults must be at the trailing parameters; you cannot have a default in the middle of the parameter list:
void f(int a = 1, int b); // ERROR: trailing parameter without default
void f(int a, int b = 1); // OK
Defaults are part of the function declaration; they may appear in a prototype in a header and not in the corresponding definition in the source file. They participate in overload resolution: a function with default arguments is a viable candidate for any call that supplies at least the non-defaulted arguments.
Function overloading
Two or more functions in the same scope may share a name as long as they differ in their parameter types:
void print(int n);
void print(double d);
void print(const std::string &s);
print(42); // calls print(int)
print(3.14); // calls print(double)
print("hello"); // calls print(const std::string&) — implicit conversion
Overload resolution picks the best match among the candidates. The standard defines an elaborate ranking — exact match, integral promotion, integral or floating conversion, user-defined conversion, ellipsis match — that selects the candidate requiring the fewest conversions. Ambiguity (no single best match) is a compile-time error.
Two functions cannot differ only in their return type:
int compute(double x);
long compute(double x); // ERROR: redeclaration with different return type
Overloading is resolved at the call site; the same function name may resolve to different functions in different contexts.
Variadic functions and <cstdarg>
C-style variadic functions remain available:
#include <cstdarg>
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; ++i) total += va_arg(args, int);
va_end(args);
return total;
}
The mechanism is type-unsafe and requires the function to determine the number and types of arguments by some out-of-band convention (a count, a format string, a sentinel). Modern C++ rarely uses it; the alternatives are:
- Variadic templates: a parameter pack
Args...accepts any number of arguments, with full type information at each position. Theprintf-replacementstd::formatis implemented this way. - Initialiser lists: a function taking
std::initializer_list<T>accepts any number of arguments of typeT. - Fixed parameters with default values: when the variability is at the upper end and small.
template <typename... Args>
void log(const char *fmt, Args &&... args) {
std::cerr << std::format(fmt, std::forward<Args>(args)...);
}
double sum(std::initializer_list<double> values) {
return std::accumulate(values.begin(), values.end(), 0.0);
}
double total = sum({1.0, 2.0, 3.0, 4.0, 5.0});
The variadic-template form is the preferred mechanism for type-safe variadic functions; the initialiser-list form is the conventional choice when all arguments share a common type.
Lambdas
C++11 introduced lambda expressions: anonymous function objects that may be passed as arguments, stored in variables, or returned from functions. The syntax:
auto square = [](int x) { return x * x; };
int n = square(5); // 25
std::vector<int> values = {1, 2, 3, 4, 5};
auto count_above = [threshold = 3](int v) { return v > threshold; };
auto count = std::count_if(values.begin(), values.end(), count_above);
A lambda expression has four parts:
- Capture clause
[…]— what to capture from the enclosing scope. - Parameter list
(…)— the function’s parameters. - Specifiers and trailing return type
mutable noexcept -> T— optional. - Body
{ … }— the function body.
Capture
The capture clause specifies how the lambda accesses names from the enclosing scope:
int x = 10;
auto by_value = [x]() { return x; }; // copies x
auto by_reference = [&x]() { return x; }; // references x
auto rename = [n = x]() { return n; }; // captures x as n (C++14)
auto move_capture = [s = std::move(my_string)]() { /* ... */ }; // C++14: move-capture
auto capture_all_value = [=]() { return x + y; }; // captures all by value
auto capture_all_reference = [&]() { return x + y; }; // captures all by reference
The conventional discipline:
- Default to capturing by value (
[x]) for short-lived lambdas. - Capture by reference (
[&x]) only when the lambda’s lifetime is contained within the captured object’s lifetime. - Avoid
[=]and[&](the catch-all forms); enumerating captures is clearer and prevents inadvertent dangling references. - Use init-capture (
[name = expr]) for move-capture and for renaming.
Generic lambdas
C++14 admitted auto parameters, making the lambda generic:
auto add = [](auto a, auto b) { return a + b; };
int n = add(1, 2); // both auto deduced as int
double d = add(1.5, 2.5); // both auto deduced as double
auto s = add(std::string{"hello, "}, std::string{"world"});
The generic lambda is implemented as a class with a templated operator(); each invocation instantiates the template for the argument types.
mutable lambdas
By default, a lambda’s operator() is const: it cannot modify its captured-by-value variables. The mutable specifier removes the const:
auto counter = [count = 0]() mutable { return ++count; };
int a = counter(); // 1
int b = counter(); // 2
The construction is the conventional way to write a stateful lambda.
Function objects (functors)
Any class with an operator() is a function object (or functor). Lambdas are syntactic sugar for one specific kind:
struct Adder {
int delta;
int operator()(int x) const { return x + delta; }
};
Adder plus_5{5};
int n = plus_5(10); // 15
std::transform(values.begin(), values.end(), values.begin(), Adder{1});
The construction is largely superseded by lambdas, but it remains useful when the function object needs additional methods, a substantial set of state, or to be friend-declared. The standard library defines several function objects in <functional> (std::less, std::greater, std::plus, std::minus, std::equal_to).
std::function
std::function<R(Args...)> is a type-erased wrapper around any callable: a function pointer, a lambda, a function object, a member function pointer combined with an instance. It is the standard mechanism for storing callables in containers and for accepting callbacks in non-template interfaces:
#include <functional>
std::vector<std::function<int(int)>> ops = {
[](int x) { return x + 1; },
[](int x) { return x * 2; },
[](int x) { return x * x; },
};
for (const auto &op : ops) std::cout << op(5) << ' ';
// 6 10 25
std::function carries overhead: the type erasure typically allocates (for non-small callables), and each call goes through a virtual-like indirection. For known callable types or for inline callbacks, a direct lambda or template parameter is preferable; std::function is appropriate when the callable type genuinely varies at runtime.
Member functions
A member function is a function defined inside a class:
class Counter {
public:
void increment();
int value() const;
void reset();
private:
int count_ = 0;
};
void Counter::increment() { ++count_; }
int Counter::value() const { return count_; }
void Counter::reset() { count_ = 0; }
The treatment of member functions is in Classes and OOP; a few specifiers warrant mention here.
const member functions
A member function declared const may be invoked on a const instance and may not modify the object:
class Point {
public:
double x() const { return x_; } // const member: no modification
void set_x(double v) { x_ = v; } // non-const member: modification
private:
double x_;
};
const Point p{3.0};
double v = p.x(); // OK: x() is const
// p.set_x(0.0); // ERROR: set_x is not const
The const qualifier on a member function is part of its type; const and non-const overloads of the same name may coexist:
class Container {
public:
int &operator[](size_t i) { return data_[i]; }
const int &operator[](size_t i) const { return data_[i]; }
private:
std::vector<int> data_;
};
Reference-qualified member functions
C++11 admitted & and && qualifiers on member functions, selecting based on whether the object is an lvalue or rvalue:
class Holder {
public:
std::string data() const & { return data_; } // for lvalues: copy
std::string data() && { return std::move(data_); } // for rvalues: move
private:
std::string data_;
};
The construction admits efficient retrieval from temporaries; calling make_holder().data() on a temporary moves the string out.
Function specifiers
| Specifier | Effect |
|---|---|
inline | A hint that the function may be inlined at the call site; admits multiple definitions across translation units. |
constexpr | The function may be evaluated at compile time when its arguments are constant expressions. |
consteval | The function must be evaluated at compile time; the value at runtime is ill-formed. (C++20) |
static | At namespace scope: internal linkage. At class scope: a class-level (non-instance) function. |
virtual | Permits dynamic dispatch through a base-class pointer or reference. |
override | (Contextual) The function overrides a base-class virtual function. The compiler checks. |
final | (Contextual) The function may not be overridden in derived classes. |
[[noreturn]] | The function does not return. |
[[nodiscard]] | A diagnostic is issued if the return value is ignored. |
constexpr int square(int x) { return x * x; }
consteval int cube(int x) { return x * x * x; }
[[nodiscard]] std::optional<Result> compute();
[[noreturn]] void fatal(std::string_view message);
Trailing return types
The trailing-return-type form moves the return type to after the parameter list:
auto compute(int x, int y) -> int { return x + y; }
The form is principally useful when the return type depends on the parameter types, in which case the parameter names are in scope:
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
C++14 generalised return-type deduction so that auto alone (without decltype) suffices for most cases:
template <typename T, typename U>
auto add(T a, U b) {
return a + b;
}
The trailing-return form remains conventional for the cases where SFINAE-based return-type expression is desired or for cosmetic consistency with lambda syntax.
Coroutines briefly
C++20 introduced coroutines: functions that may suspend execution and resume later. A function becomes a coroutine if it contains co_await, co_yield, or co_return:
generator<int> count_to(int n) {
for (int i = 0; i < n; ++i) {
co_yield i;
}
}
for (int v : count_to(10)) {
std::cout << v << ' ';
}
The full mechanism is substantial: a coroutine’s return type must satisfy a promise type protocol that the standard library does not yet provide a default implementation of. Practical use of coroutines requires a coroutine library (the proposed std::generator is part of C++23; <future> integrates with coroutines for asynchronous tasks). The full treatment is beyond this page’s scope; the principal point is that the language admits coroutines and the surface is co_await/co_yield/co_return.
A note on what C++ does not have
C++ does not have:
- Named arguments. Calls supply arguments positionally; the conventional substitute is a parameter struct passed by value with designated initialisers.
- Keyword arguments in the Python or Rust sense.
- Rest parameters in the JavaScript sense; variadic templates are the closest analogue.
- First-class function types in the same sense as ML or Haskell; functions are values (via lambdas or function pointers) but the type system distinguishes function pointers from function objects from
std::function.
The combination of overloading, default arguments, lambdas, templates, and std::function covers most of what these features would provide; the surface is wider but the conventions are well-established.