Skip to content
C Codeloom
C++

C++ Lambda Expressions and Captures

Master C++ lambdas: syntax, capture modes by value and reference, mutable lambdas, generic lambdas, and lifetime pitfalls.

·3 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • Lambda syntax
  • Capture by value vs reference
  • Mutable lambdas
  • Generic lambdas
  • Lifetime traps

Prerequisites

  • Basic familiarity with C++

What and Why

C++11 introduced lambdas to give you function objects without the boilerplate of writing a class with operator(). They are essential for STL algorithms, custom comparators, callbacks, and any code where you want to pass behavior around.

A lambda is just sugar for an anonymous class with a call operator and (optionally) captured state.

Mental Model

The compiler rewrites [x](int y) { return x + y; } into roughly this:

struct __Lambda12 {
  int x;
  int operator()(int y) const { return x + y; }
};

Captures become member variables. The mutable keyword removes the const on operator(). Generic lambdas (auto parameters) become a templated operator().

[ ]        no captures
[=]        capture all used variables by value (copy)
[&]        capture all used variables by reference
[x, &y]    x by value, y by reference
[=, &z]    everything by value, z by reference
[this]     capture enclosing object pointer
[*this]    capture a copy of the enclosing object (C++17)
Capture modes

Hands-on Example

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

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

  // Count by value capture
  auto count = std::count_if(nums.begin(), nums.end(),
    [threshold](int n) { return n > threshold; });

  // Mutable lambda accumulating state
  auto running_sum = [sum = 0](int n) mutable {
    sum += n;
    return sum;
  };
  for (int n : nums) std::cout << running_sum(n) << " ";

  // Generic lambda (C++14)
  auto print = [](const auto& x) { std::cout << x << "\n"; };
  print(42);
  print("hello");
}

The [sum = 0] is an init-capture (C++14): you create a new variable scoped to the lambda.

Common Pitfalls

Dangling references. [&] captures by reference, so storing the lambda beyond the scope of the captured variable is undefined behavior:

std::function<int()> make_bad() {
  int x = 10;
  return [&]() { return x; }; // x dies, lambda holds dangling ref
}

Capture by value when the lambda outlives the enclosing scope.

Capturing this accidentally. Inside a member function, [=] historically captured this by pointer (giving you access to all members). If the object dies before the lambda runs, you have undefined behavior. Since C++20, [=, this] is required to be explicit. Use [*this] (C++17) to capture a copy of the whole object.

Reference captures of loop variables. Spawning lambdas in a loop with [&i] and storing them leaves them all referring to the same i.

Large captures. Each by-value capture is a copy. Capturing a 1MB std::vector by value into a lambda passed to std::async makes a copy every call. Capture by reference (carefully) or move with [v = std::move(v)].

Practical Tips

  • Default to [=] for short-lived callbacks; default to explicit captures [x, &y] for long-lived ones.
  • Use init-captures to move-only types into a lambda: [p = std::move(uniquePtr)]() { ... }.
  • For STL algorithms with simple predicates, prefer named lambdas stored in a const auto for readability.
  • constexpr lambdas (C++17) let you use them in constexpr contexts.
  • C++20 templated lambdas ([]<typename T>(T x) { ... }) give you typename access without decltype.

Wrap-up

C++ lambdas are powerful but unforgiving about lifetimes. Always ask: what does this lambda capture, and will those things still be alive when the lambda runs? Once that becomes second nature, lambdas turn STL algorithms, ranges, and async code into clean, expressive C++ instead of verbose functor classes.