Lambdas & Closures Under the Hood
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
The compiler creates a unique, anonymous struct (closure type) with member variables corresponding to each captured variable. Variables captured by value become value fields; variables captured by reference become reference fields (T&, represented as 8-byte pointers). The lambda body is compiled as the struct's operator() member function (const by default). A stateless lambda also receives an implicit conversion operator to a function pointer.
[this] captures a copy of the pointer to the current object. If the enclosing object is destroyed before an asynchronous callback executes, dereferencing member variables through this-> results in a use-after-free crash.
C++17 introduced [*this], which creates a full independent copy of the object by value inside the closure. The callback remains completely safe even if the original object is destroyed.
Lambdas default to operator() const to preserve value semantics and prevent accidental mutations of captured local state. The mutable keyword removes const from the generated operator(), allowing the closure to modify its internal value-captured fields across calls, effectively turning the lambda into a stateful functor object.
Interviewer's lens
Lambdas are omnipresent in modern C++, but weak candidates view them as mysterious syntax sugar. When asked what a lambda actually is in memory, they stumble. Strong candidates explain that every lambda expression generates a unique, anonymous closure class with member variables for captured state and an operator() const, dissect dangling reference bugs in async callbacks, demonstrate how C++14 init-capture enables move-only types like std::unique_ptr in closures, and articulate the runtime cost of std::function (type erasure, virtual dispatch indirection, and heap allocation beyond SBO limits) versus zero-cost template callables.
Under the hood & memory layout
At compile time, a lambda expression is lowered into a unique, unnamed struct (the closure type). Variables captured by value become member fields of that struct; variables captured by reference become reference members (T&, which occupy an 8-byte pointer slot at the ABI level). The lambda body becomes operator() inside that struct, marked const by default (meaning value-captured fields cannot be modified unless the lambda is declared mutable). A stateless lambda ([]) has a size of 1 byte (per standard C++ empty-struct rules) and generates a non-member conversion operator to a raw C function pointer (+lambda). By contrast, std::function is a heavy type-erased wrapper: it stores a function pointer table (or vtable) and an internal storage buffer. If a closure fits in its Small Buffer Optimization (SBO) union (typically 16–32 bytes), no heap allocation occurs; if the closure exceeds that threshold, std::function allocates a heap block via operator new, incurring dynamic allocation and cache-miss overhead on every invocation.
A closure capturing int a and int& b layout: int a_ (4 bytes) + padding (4 bytes) + int* b_ref (8 bytes) = 16 bytes total on 64-bit systems.
[=] deep-copies variables into closure members. [&] stores 8-byte addresses referencing the caller's stack frame; if the caller's frame is popped before execution, reading these references yields Undefined Behavior.
By default, operator() const prevents mutating value-captured members. Marking a lambda mutable strips const from operator(), allowing the closure to mutate its internal fields across calls.
Contains a union: union { void* heapPtr; char inlineBuffer[16]; } plus a control vtable pointer (8 bytes). Closures <= 16 bytes reside entirely on the stack; larger closures trigger dynamic heap allocations.
Core concepts & syntax traps
Anatomy of a Lambda: The Compiler's Anonymous Closure Struct
Anatomy of a Lambda: The Compiler's Anonymous Closure Struct
A C++ lambda is not a function pointer; it is syntactic shorthand for instantiating an anonymous closure struct. When you write:
int factor = 3;
auto multiply = [factor](int x) { return x * factor; };
The compiler generates:
class __Lambda_12_7 {
int factor_;
public:
__Lambda_12_7(int factor) : factor_(factor) {}
int operator()(int x) const { return x * factor_; }
};
auto multiply = __Lambda_12_7(factor);
Because operator() is marked const by default, captured members cannot be modified inside the lambda body.
int a = 10, b = 20;
auto l = [a, &b](int x) { return x + a + b; };
// sizeof(l) is exactly sizeof(int) + sizeof(int&) + alignment padding = 16 bytes!A lambda is a compiler-generated class with captured variables as fields and operator() const.
Capture Modes & The Dangling Reference Trap ([=], [&], [this], [*this])
Capture Modes & The Dangling Reference Trap ([=], [&], [this], [*this])
Choosing the wrong capture mode is one of the most prevalent causes of undefined behavior in multi-threaded and async code:
[=](Value Capture): Copies variables into the closure struct. Safe for async, but copies large structures.[&](Reference Capture): Stores 8-byte addresses referencing local variables. Dangling Trap: If the lambda outlives the enclosing function (e.g. passed to a thread or event loop), the references point into dead stack frames![this]: Captures the rawthispointer by value. If the enclosing object is deleted before the callback runs, accessing members viathis->triggers a use-after-free crash.[*this](C++17): Captures a full copy of*thisby value, decoupling the callback's lifetime from the original object.
struct Service {
std::string endpoint = "https://api.internal";
auto getHandler() {
// DANGEROUS: [this] dangles if Service is deleted before handler runs!
// return [this]() { return endpoint; };
// SAFE: [*this] captures independent copy of Service
return [*this]() { return endpoint; };
}
};Never capture local variables by reference [&] or capture [this] in callbacks that can outlive the calling stack frame.
C++14 Generalized Init-Capture: Moving Move-Only Types into Closures
C++14 Generalized Init-Capture: Moving Move-Only Types into Closures
Prior to C++14, you could only capture existing variables by value or reference. This made it impossible to pass move-only types (like std::unique_ptr, std::promise, or socket handles) into a lambda by value.
C++14 introduced Init-Capture (Generalized Lambda Capture) using syntax [var = expression]. This moves resources directly into the closure struct with zero copying.
auto uptr = std::make_unique<int>(42);
// Moves ownership of uptr into the closure field 'p':
auto worker = [p = std::move(uptr)]() {
return *p * 10;
};
// uptr is now nullptr in the caller scope!Use init-capture [p = std::move(uptr)] to transfer ownership of move-only resources into a lambda closure.
Mutable Lambdas: Removing const from operator()
Mutable Lambdas: Removing const from operator()
By default, the compiler declares operator() const on the closure struct, meaning value-captured members cannot be modified. If you attempt [count = 0]() { ++count; }, the compiler throws an error: assignment of member in read-only object.
Adding mutable removes const from the generated operator(), allowing the closure to maintain mutable internal state across consecutive invocations. Note that if the closure is copied, the copy gets its own independent clone of that state.
auto counter = [count = 0]() mutable {
return ++count;
};
std::println("{} {}", counter(), counter()); // Prints: 1 2The mutable keyword removes const from operator(), turning the lambda into a stateful functor.
std::function Overhead: Type Erasure & Small Buffer Optimization (SBO)
std::function Overhead: Type Erasure & Small Buffer Optimization (SBO)
std::function<Ret(Args...)> is a general-purpose polymorphic function wrapper capable of holding any callable (lambda, function pointer, member function, functor). However, this flexibility comes with significant runtime overhead:
- Virtual Indirection: Invocations execute via an indirect function pointer call (cannot be easily inlined by the optimizer).
- Memory Size:
sizeof(std::function)is typically 32–48 bytes. - Heap Allocation: If the captured closure exceeds the internal Small Buffer (typically 16–32 bytes),
std::functiondynamically allocates memory on the heap!
When writing high-performance APIs or hot loops, prefer unconstrained template callables template <typename F> void invoke(F&& f) or auto parameters, which inline with zero overhead, zero heap allocations, and zero indirection.
// SLOW: type-erased wrapper, potential heap allocation, indirect call
void runCallback(std::function<void()> cb) { cb(); }
// FAST: zero-overhead template callable, perfectly inlined by compiler
template <typename F>
void runFast(F&& cb) { cb(); }Use template callables for zero-overhead inlined loops; reserve std::function for heterogeneous storage or ABI boundaries.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.