Lambdas

C++ · Practical · 12 min

Lambdas are how you customise algorithms, write callbacks and launch threads in modern C++.

A lambda is an anonymous function object written inline: [captures](params) { body }. The return type is deduced, or given as -> T.

The capture list says which outside variables the lambda can use:

  • [x] copies x into the lambda when it's created; [&x] refers to the original.
  • [=] and [&] capture everything used, by copy or by reference. Prefer listing captures explicitly in longer code.
  • Init-captures create new members: [n = 0], or [p = std::move(ptr)] to move a unique_ptr in.
  • In a member function, [this] captures the object pointer and [*this] copies the object.

Captured copies are const inside the lambda unless you mark it mutable, which lets a lambda keep its own state between calls.

Each lambda has its own unique, unnamed type, so store one in auto. With auto parameters you get a generic lambda, a template in disguise; C++20 also allows an explicit template parameter list, []<typename T>(T x) { ... }. Lambdas with no captures convert to plain function pointers, handy for C APIs.

To store different callables in one variable or container, such as a list of callbacks, use std::function<int(int)> from <functional>. It type-erases any callable with that signature, at the cost of a possible heap allocation and an indirect call. Templates or auto parameters avoid that cost when you just pass a lambda to a function. C++23 adds std::move_only_function for callables that can't be copied.

The main danger: a lambda that captures by reference must not outlive the variables it captured. That bites with callbacks, threads and lambdas returned from functions; capture by value there.

Example

#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>
#include <vector>

int main() {
    std::vector<int> v{5, 2, 8, 1};
    int threshold = 3;

    auto above = [threshold](int x) { return x > threshold; };     // capture by value
    std::cout << std::count_if(v.begin(), v.end(), above) << '\n';  // 2

    int calls = 0;
    std::sort(v.begin(), v.end(), [&calls](int a, int b) { ++calls; return a > b; });
    std::cout << v[0] << ' ' << (calls > 0) << '\n';                // 8 1

    auto add = [](auto a, auto b) { return a + b; };                // generic lambda
    std::cout << add(1, 2) << ' ' << add(1.5, 2.0) << '\n';         // 3 3.5

    auto counter = [n = 0]() mutable { return ++n; };               // own state
    counter(); counter();
    std::cout << counter() << '\n';                                 // 3

    auto owned = [p = std::make_unique<int>(7)] { return *p; };     // move-only capture
    std::function<int(int)> twice = [](int x) { return 2 * x; };    // type-erased
    std::cout << owned() + twice(4) << '\n';                        // 15
}

Remember

  • [x] copies at creation, [&x] refers to the live variable
  • Use mutable to modify by-value captures; init-captures [p = std::move(x)] move things in
  • std::function stores any callable but costs more than auto/templates

Watch out for

  • Capturing locals by reference in a callback or thread that runs after the function has returned.

Check yourself

  1. What does this print? `int x = 1; auto f = [x] { return x; }; x = 5; std::cout << f();`
  2. Why does `[n = 0]() { return ++n; }` fail to compile?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/lambda