Polyglot
Languages C++ functions
C++ § functions

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++:

NeedTake as
Read-only access to a valueconst T& (or T for small types)
Modify the caller’s valueT&
Take ownership of a movableT (by value)
Optional accessconst T* (raw pointer; nullable)
Read-only stringstd::string_view
Read-only contiguous rangestd::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. The printf-replacement std::format is implemented this way.
  • Initialiser lists: a function taking std::initializer_list<T> accepts any number of arguments of type T.
  • 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:

  1. Capture clause […] — what to capture from the enclosing scope.
  2. Parameter list (…) — the function’s parameters.
  3. Specifiers and trailing return type mutable noexcept -> T — optional.
  4. 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

SpecifierEffect
inlineA hint that the function may be inlined at the call site; admits multiple definitions across translation units.
constexprThe function may be evaluated at compile time when its arguments are constant expressions.
constevalThe function must be evaluated at compile time; the value at runtime is ill-formed. (C++20)
staticAt namespace scope: internal linkage. At class scope: a class-level (non-instance) function.
virtualPermits 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.