Closures and Captures
English

Closures and Captures

After generic functions, concepts, and lifetimes, the next practical step is local callback code.

In scpp today, closures reuse ordinary C++ lambda syntax. The important mental model is simple: a closure is a small anonymous object, and its capture list describes what data that object stores or borrows.

For each runnable example below, save the file as closures.scpp, then build and run it like this:

scpp closures.scpp -o closures
./closures

A closure can be passed to a generic callback parameter

The most common use is to hand a short local action to another function.

import std;

template<typename T>
concept IntConsumer = requires(T f, int x) { f(x); };

void for_each_doubled(std::span<int> s, IntConsumer auto&& f) {
    int i = 0;
    while (i < s.size) {
        f(s[i] * 2);
        i = i + 1;
    }
    return;
}

int main() {
    int arr[3];
    arr[0] = 1;
    arr[1] = 2;
    arr[2] = 3;
    std::span<int> s = arr;
    int sum = 0;
    for_each_doubled(s, [&sum](int x) { sum = sum + x; });
    std::println("{}", sum);
    return 0;
}

Output:

12

The closure stores a mutable-reference capture of sum, so each callback invocation updates the same outer variable.

Mixed capture forms let one closure borrow some values and copy others

Capture lists are explicit about which outer names become owned fields and which become borrowed reference fields.

import std;

template<typename T>
concept IntTransform = requires(T f, int x) { f(x); };

int apply(IntTransform auto&& f, int z) {
    return f(z);
}

int main() {
    int a = 5;
    int b = 10;
    int first = apply([&, a](int z) -> int { return a + b + z; }, 3);
    int second = apply([=, &b](int z) -> int { return a + b + z; }, 3);
    std::println("{}", first);
    std::println("{}", second);
    return 0;
}

Output:

18
18

[&, a] means “borrow everything else, but copy a by value”. [=, &b] means the opposite: “copy everything else, but borrow b by reference”.

Methods must capture this explicitly

Inside a method, a closure may call back into the receiver, but this must be named explicitly in the capture list.

import std;

template<typename T>
concept IntTransform = requires(T f, int x) { f(x); };

int apply(IntTransform auto&& f, int z) {
    return f(z);
}

class Multiplier {
public:
    virtual ~Multiplier() { return; }

private:
    int factor_{};

public:
    Multiplier(int factor) : factor_{factor} {
        return;
    }

    int scale(int x) const {
        return x * this->factor_;
    }

    int use_closure(int z) {
        return apply([this](int value) -> int { return this->scale(value); }, z);
    }
};

int main() {
    Multiplier m{7};
    int answer = m.use_closure(3);
    std::println("{}", answer);
    return 0;
}

Output:

21

That explicit [this] keeps the lifetime edge visible. A closure inside a method does not get an implicit escape hatch around borrowing rules.

The working model for closures in today’s scpp

So far, the practical rules are:

That is enough to write ordinary callback-style code with today’s scpp. The next section can stay in the same area and add generic lambdas.


← Previous: Validating References with Lifetimes · Table of Contents