Polyglot
Languages C++ functional
C++ § functional

Functional and ranges

C++ admits functional-style programming through several mechanisms — first-class functions via lambdas and std::function, the algorithms library that operates on iterator pairs, and (since C++20) ranges and views that compose lazily into pipelines. The language is not purely functional and does not enforce immutability, but the patterns it supports cover most of the practical use cases. The combination of the algorithms library and the ranges library is, in practice, the C++ idiom for higher-order operations: a substantial fraction of explicit loops in idiomatic code is replaceable by an algorithm or a view-based pipeline, often with clearer intent at the call site.

This page covers first-class functions, the algorithms library and the iterator interface, the C++20 ranges library, the value-disjunction types (std::optional, std::variant, std::expected), pure-function discipline, and the differences from purely functional languages.

First-class functions and lambdas

Functions are values in C++ in two senses: through function pointers (and member-function pointers) and through lambda expressions and function objects. Both are first-class — they may be assigned, copied, passed, returned, stored.

// Function pointer
int (*fp)(int) = [](int x) { return x * x; };

// Lambda
auto square = [](int x) { return x * x; };

// std::function
std::function<int(int)> f = square;

Lambdas are syntactic sugar for an anonymous class with an operator(); the compiler generates the class once per lambda. The treatment of lambdas and the capture-clause is in Functions; this page covers their use in higher-order contexts.

The algorithms library

The <algorithm> and <numeric> headers provide a substantial set of generic operations on iterator ranges. The fundamental shape is a function that takes a [first, last) pair and a callable:

#include <algorithm>
#include <numeric>
#include <vector>

std::vector<int> v = {1, 2, 3, 4, 5};

// Reductions
auto sum  = std::accumulate(v.begin(), v.end(), 0);                    // 15
auto prod = std::accumulate(v.begin(), v.end(), 1, std::multiplies{}); // 120

// Searches
auto it_5 = std::find(v.begin(), v.end(), 5);
auto it_p = std::find_if(v.begin(), v.end(), [](int x) { return x > 3; });

// Counting
auto count_evens = std::count_if(v.begin(), v.end(),
                                  [](int x) { return x % 2 == 0; });

// Transformations (in-place)
std::transform(v.begin(), v.end(), v.begin(),
                [](int x) { return x * 2; });

// Selection / partitioning
std::sort(v.begin(), v.end());
auto last_unique = std::unique(v.begin(), v.end());
v.erase(last_unique, v.end());

The full set of algorithms covers searching (find, find_if, binary_search, lower_bound), counting (count, count_if), comparison (equal, mismatch, lexicographical_compare), modification (copy, move, transform, replace, fill), removal (remove, remove_if, unique), sorting (sort, partial_sort, nth_element, stable_sort), set operations (includes, set_union, set_intersection, set_difference), heap operations (push_heap, pop_heap, make_heap), permutations (next_permutation, prev_permutation), and reductions (accumulate, reduce, inner_product, partial_sum).

The algorithms compose through the iterator abstraction: any range that exposes the appropriate iterator category — input, forward, bidirectional, random-access — admits the corresponding algorithms.

Higher-order operations

Three patterns recur:

Map (std::transform)

std::vector<int> doubled(v.size());
std::transform(v.begin(), v.end(), doubled.begin(),
                [](int x) { return x * 2; });

// Or in-place:
std::transform(v.begin(), v.end(), v.begin(),
                [](int x) { return x * 2; });

Filter (std::copy_if)

std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
              [](int x) { return x % 2 == 0; });

Fold (std::accumulate, std::reduce)

auto total = std::accumulate(v.begin(), v.end(), 0);
auto product = std::accumulate(v.begin(), v.end(), 1, std::multiplies{});

auto custom = std::accumulate(words.begin(), words.end(), std::string{},
                                [](std::string acc, const std::string &w) {
                                    return acc.empty() ? w : acc + ", " + w;
                                });

std::reduce (C++17) is a parallelisable variant of std::accumulate that requires the operation to be associative and commutative.

Ranges (C++20)

The <ranges> library introduces ranges — types that expose begin()/end() (or model the appropriate concept) — and range-based algorithms that take a range directly rather than a pair of iterators:

#include <ranges>
#include <algorithm>

std::vector<int> v = {1, 2, 3, 4, 5};

auto count_evens = std::ranges::count_if(v, [](int x) { return x % 2 == 0; });

std::ranges::sort(v);

auto it = std::ranges::find(v, 3);

The range-based forms eliminate the boilerplate of explicit iterator pairs. Most algorithms in <algorithm> have a corresponding std::ranges:: form.

Views and view composition

The principal innovation of the ranges library is views: lazy, composable adaptors that transform a range without materialising the result:

#include <ranges>

namespace rv = std::views;

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

auto pipeline =
    v
    | rv::filter([](int x) { return x % 2 == 0; })   // {2, 4, 6, 8, 10}
    | rv::transform([](int x) { return x * x; })     // {4, 16, 36, 64, 100}
    | rv::take(3);                                    // {4, 16, 36}

for (int x : pipeline) std::cout << x << ' ';
// 4 16 36

Each view is a thin wrapper that materialises elements on demand; intermediate vectors are not allocated. The compiler typically inlines the entire pipeline into a single loop.

The standard views include:

ViewEffect
rv::filter(pred)Keep elements where pred(x) is true.
rv::transform(f)Apply f to each element.
rv::take(n)First n elements.
rv::drop(n)Skip first n elements.
rv::take_while(pred)Elements until pred first fails.
rv::drop_while(pred)Elements after pred first fails.
rv::reverseReverse iteration.
rv::keysMap’s keys.
rv::valuesMap’s values.
rv::iota(n, m)The integers [n, m).
rv::iota(n)An infinite sequence starting at n.
rv::zip(a, b, ...)(C++23) Element-wise tupling.
rv::enumerate(C++23) Index-and-element pairs.
rv::joinConcatenate a range of ranges.
rv::chunk(n)(C++23) Chunks of size n.
rv::split(delim)Split by a delimiter.

C++23 added std::ranges::to<C>() to materialise a view into a concrete container:

auto squares = rv::iota(1, 6)
             | rv::transform([](int x) { return x * x; })
             | std::ranges::to<std::vector>();
// std::vector<int>{1, 4, 9, 16, 25}

The composition is the C++20+ idiom for what map-filter-reduce expresses in functional languages. Pre-C++20 code accomplishes the same with explicit loops or with chains of algorithm calls; the views syntax is substantially more compact and typically faster (a single pass through the data).

Value disjunction: std::optional, std::variant, std::expected

Three standard-library types let values express “absent”, “one of several alternatives”, and “value or error” without resorting to pointers or exceptions.

std::optional<T>

Represents an optional T: either a T or nothing.

#include <optional>

std::optional<int> parse_int(std::string_view s) {
    int value = 0;
    auto [end, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec != std::errc{}) return std::nullopt;
    return value;
}

if (auto v = parse_int("42")) {
    std::cout << *v << '\n';
}

std::optional is the conventional return type for fallible operations whose only failure mode is absence — search functions, parsers, accessors that may not find their target.

C++23 added monadic operations: transform, and_then, or_else. They admit chaining without explicit checks:

std::string greet = parse_int(input)
    .transform([](int n) { return "value " + std::to_string(n); })
    .or_else([] { return std::optional<std::string>{"no value"}; })
    .value();

std::variant<T1, T2, …>

Represents one of several alternatives:

#include <variant>

std::variant<int, std::string, double> v;
v = 42;
v = "hello";
v = 3.14;

double area(const std::variant<Circle, Square> &shape) {
    return std::visit([](const auto &s) { return s.area(); }, shape);
}

The full treatment of std::variant and std::visit is in Pattern matching. For functional-style code, variant is the conventional encoding of a sum type — a tagged union with type-safe access and exhaustive dispatch.

std::expected<T, E>

Represents a T (success) or an E (error). Introduced in C++23:

#include <expected>

std::expected<int, std::string> parse_int(std::string_view s) {
    int value = 0;
    auto [end, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec != std::errc{}) return std::unexpected{"parse error"};
    return value;
}

auto result = parse_int(input);
if (result) {
    std::cout << *result << '\n';
} else {
    std::cerr << result.error() << '\n';
}

std::expected is the value-level alternative to exceptions: the function returns the success or the error, the caller dispatches on the value. Like optional, it admits monadic chaining (transform, and_then, or_else).

Pure-function discipline

C++ does not enforce purity; functions may freely modify global state, perform I/O, and read clocks. Nonetheless, the discipline of writing pure functions — functions whose output depends only on their arguments and that have no side effects — has value:

  • Pure functions are easier to test (no fixtures, no mocks).
  • Pure functions are easier to reason about.
  • Pure functions admit memoisation, parallelisation, and constant folding.
  • Pure functions are conventionally constexpr-compatible.

The conventional discipline:

  • Mark pure functions [[nodiscard]] so the compiler warns if their result is ignored.
  • Mark pure functions constexpr when their bodies admit it.
  • Take parameters by value or const reference, not by mutable reference.
  • Return by value rather than mutating an out-parameter.
[[nodiscard]] constexpr double mean(std::span<const double> values) {
    return std::accumulate(values.begin(), values.end(), 0.0) / values.size();
}

The discipline is stylistic — the language does not enforce it — but it produces code that is easier to read, compose, and test.

The [[nodiscard]] attribute

[[nodiscard]] (C++17) marks a function whose return value should not be discarded:

[[nodiscard]] std::optional<Result> compute();

compute();          // diagnostic: discarding [[nodiscard]] return value
auto r = compute(); // OK

The attribute is the conventional defence against accidentally ignoring the result of a fallible operation. C++20 admitted a string argument to clarify the diagnostic:

[[nodiscard("call discards the parsed value")]]
std::optional<int> parse_int(std::string_view s);

The attribute applies equally to free functions, member functions, and types: a [[nodiscard]] type warns when an instance is discarded. The standard’s std::expected and std::optional are not themselves [[nodiscard]], but most user-defined error-carrying types should be.

Functional patterns vs OOP patterns

Many problems admit both an object-oriented solution (a class hierarchy with virtual dispatch) and a functional solution (a closed sum type with pattern-matching dispatch). The conventional advice in modern C++:

  • Use OOP when the set of operations is fixed and the set of types is open: many concrete types implement the same set of methods. Classic example: GUI widgets, drivers, plugins.
  • Use functional/variant when the set of types is fixed and the set of operations is open: a few concrete types, with new operations added without modifying the types. Classic example: AST nodes (the kinds are fixed; operations like type-checking, evaluation, pretty-printing are added independently).
  • Use templates when the set of types is open at compile time: the operations are known but the types are determined by the caller. Classic example: containers and algorithms.

The choice is a design decision; the language admits both, and the conventional discipline is to pick the one that matches the axis of variation.

A note on what C++ does not have

The features that purely functional languages rely on, and their status in C++:

FeatureAvailable?
First-class functionsYes (lambdas, function pointers, std::function)
ClosuresYes (lambdas with capture)
Higher-order functionsYes (algorithms, ranges)
Currying / partial applicationManual (via std::bind or lambdas)
Immutable dataLimited (const); not a separate type system
Algebraic data typesThrough std::variant
Pattern matchingNo (approximated; see Pattern matching)
Tail-call optimisationImplementation-defined; not guaranteed
Persistent data structuresNo standard-library support
Lazy evaluationThrough views (C++20) for ranges; not generally

The combination of the algorithms library, the ranges library, the value-disjunction types, and the [[nodiscard]] discipline cover most of the practical functional programming use cases in C++. Code that genuinely requires the missing features (true persistence, lazy infinite lists, advanced pattern matching) is typically written in a functional language and called from C++ at a boundary.