Functions and Lambdas

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Declarations, Overloading, and Default Arguments

int add(int a, int b);
double add(double a, double b);        // overload: same name, different parameter types
int add(int a, int b, int c = 0);      // default argument -- callable as add(1, 2) or add(1, 2, 3)

// int add(int a, int b);              // error if both existed: return type alone cannot distinguish overloads

Overload resolution picks the best viable match by implicit-conversion ranking (exact match > promotion > standard conversion > user-defined conversion); an ambiguous call is a compile error, never a silent guess.

Defaulted and Deleted Functions

class NonCopyable {
public:
    NonCopyable() = default;                          // ask the compiler for its usual implementation
    NonCopyable(const NonCopyable&) = delete;           // forbid copying entirely -- a compile error, not UB
    NonCopyable& operator=(const NonCopyable&) = delete;
    NonCopyable(NonCopyable&&) = default;                // moving is still allowed
    NonCopyable& operator=(NonCopyable&&) = default;
};

void onlyIntegers(int) {}
template <typename T> void onlyIntegers(T) = delete;   // = delete also blocks a whole overload set for other T

See Classes and Objects for how = default/= delete interact with the rule of zero/three/five.

Lambdas

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    auto square = [](int x) { return x * x; };          // no captures
    int factor = 3;
    auto scale = [factor](int x) { return x * factor; };  // capture by value (a snapshot at creation)
    auto scaleRef = [&factor](int x) { return x * factor; }; // capture by reference (sees later changes)
    auto captureAll = [=]() { return factor; };            // capture everything used, by value
    auto captureAllRef = [&]() { factor++; };               // capture everything used, by reference

    std::vector<int> v = {5, 3, 1, 4, 2};
    std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });   // descending
    for (int x : v) std::cout << x << ' ';

    auto generic = [](auto a, auto b) { return a + b; };    // generic lambda (C++14): implicitly a template
    std::cout << generic(1, 2) << generic(1.5, 2.5) << '\n';

    auto templated = []<typename T>(std::vector<T> const& vec) { return vec.size(); }; // template lambda (C++20)
    std::cout << templated(v) << '\n';

    (void)square; (void)scale; (void)scaleRef; (void)captureAll; (void)captureAllRef;
}

Recursive Lambdas

A lambda cannot name itself directly, but C++23’s deducing this lets it take a self-parameter:

auto factorial = [](this auto self, int n) -> int {
    return n <= 1 ? 1 : n * self(n - 1);
};
static_assert(factorial(5) == 120);

Before C++23, the usual workarounds were std::function with a captured reference to itself, or std::function passed by reference into the lambda’s own capture list.

std::function, std::invoke, and Function Composition

#include <functional>
#include <iostream>

int addTwo(int x) { return x + 2; }

struct Multiplier {
    int factor;
    int operator()(int x) const { return x * factor; }
};

int main() {
    std::function<int(int)> f = addTwo;      // type-erased callable: functions, lambdas, functors all fit
    f = [](int x) { return x * 10; };
    std::cout << f(5) << '\n';                // 50

    Multiplier times3{3};
    std::cout << std::invoke(times3, 5) << '\n';   // 15 -- std::invoke handles callables uniformly, including
                                                     // member function pointers and pointers-to-member-data

    auto compose = [](auto g, auto h) {
        return [g, h](auto x) { return g(h(x)); };
    };
    auto addThenDouble = compose([](int x) { return x * 2; }, addTwo);
    std::cout << addThenDouble(3) << '\n';     // (3+2)*2 = 10
}

Higher-order functions — functions taking or returning other functions, like compose above, or the map/fold-style algorithms in Iterators and Algorithms — are idiomatic in modern C++ thanks to lambdas and templates.

noexcept and [[nodiscard]]

void mayThrow();
void neverThrows() noexcept;              // promises not to throw; std::terminate if it does anyway

template <typename T>
void swapValues(T& a, T& b) noexcept(noexcept(std::swap(a, b)))   // conditional noexcept
{
    using std::swap;
    swap(a, b);
}

[[nodiscard]] int computeChecksum(int data);   // ignoring the return value is now a compiler warning

noexcept functions let containers like std::vector choose to move elements on reallocation instead of copying them (see Move Semantics and Value Categories) — the strong exception-safety guarantee otherwise requires a fallback to copying.

See Also

  • C: Functions — no overloading, no default arguments and no lambdas — function pointers carry that weight in C.