A lambda expression is a callable object defined inline where it's needed, without writing a full named function or a small functor class. Added in C++11, lambdas became the default way to pass behavior into algorithms, threads, callbacks, and anything else that takes a callable. This chapter covers what a lambda is, the parts of its syntax, how to use it with the standard library, and where to store one. Captures get a brief mention here for context; the deep dive on them lives in the next chapter.
Before lambdas, handing a small piece of behavior to a standard algorithm or a thread had two options, both unwieldy.
Suppose an online store keeps a list of orders and wants to sort them by total amount. std::sort accepts a third argument, a comparator, that decides which of two elements should come first. The first option was to write a free function:
The free function works correctly. Floating-point output doesn't show trailing zeros by default, so the actual run prints 129, 49.99, 14.5, 7.25. The free function does its job.
The downside is that byTotalDescending lives far away from the call site. Understanding what std::sort is doing requires jumping to wherever the function was defined. For a comparator that's only used once, that's a lot of code for a small idea.
The second option was a functor, a class with operator() overloaded:
This is even worse for a one-off comparator. A whole class for a single comparison. And if the comparator needs to capture state, like "sort by total only when the customer is in this country", the functor has to grow constructor parameters and member fields.
A lambda is C++'s way of writing the small thing in the place it's used:
The comparator is now right next to std::sort. A reader doesn't have to jump anywhere. The compiler still generates a class with operator(), but it isn't written manually.
The general form of a lambda is:
Most lambdas use only some of those parts. The minimum legal lambda is [](){}, which captures nothing, takes no parameters, returns nothing, and does nothing.
Each part means a specific thing. A running example walks through them.
[ ]The capture list is the only part mandatory in syntax. Even when empty, the [] must be present, because the brackets are how the compiler recognizes "this is a lambda" instead of an array subscript or something else.
A non-capturing lambda has empty brackets:
A capturing lambda lists variables from the enclosing scope that the body needs:
[saleCutoff] means "copy saleCutoff from the enclosing scope into the lambda". Inside the body, saleCutoff is a local copy. Captures are covered in a few sections. For now, they are "names from outside that the body uses".
( )Parameters work like function parameters. Types, references, defaults, all of them.
For a lambda with no parameters, C++23 allows dropping the parentheses entirely. In C++11/14/17, the parentheses are required even for an empty parameter list:
The parentheses appear explicitly in this course so the examples compile with -std=c++17.
{ }The body is a function body. Anything legal inside a function is legal inside a lambda, including loops, conditionals, declaring local variables, and calling other functions.
-> TThis is the piece most lambdas leave off. C++ deduces the return type from the body in almost every realistic case. The -> T syntax is needed only to override the deduction or when the deduction fails.
Both compile to identical code. The trailing return type syntax (-> T after the parameter list) is the only way to write a return type for a lambda. It can't go before the [ the way it would for a function. That's why this style is sometimes called "trailing return type".
When does deduction fail or matter?
If the body has multiple return statements and they don't all return the same type, the compiler refuses to deduce. Spell out the return type to break the tie:
The first and third returns produce const char*. The second produces std::string. Without the -> std::string, the compiler can't pick one and produces a long error. With the trailing return type, every return is implicitly converted to std::string.
The other case is forcing a different type than what the body produces, usually a wider type. Returning 0 deduces to int; for double, -> double makes 0 get converted.
The pieces together with names:
Reading left to right: the capture list pulls names in from outside, the parameter list defines what the lambda receives at each call, the optional trailing return type pins down what comes back, and the body is the actual code that runs. Every lambda fits this shape, even when some parts are empty.
A lambda isn't a new kind of value. When the compiler sees a lambda expression, it generates a unique, unnamed class type with a member operator(). Each lambda produces its own type, called its closure type. Each time the expression is evaluated, it constructs an object of that type, called a closure object.
This code:
is roughly equivalent to:
The exact generated name is up to the compiler and isn't visible to user code. The consequence: every lambda has its own distinct type, even if two lambdas look identical.
This is why lambdas are almost always stored in auto variables. The type can't be written by hand because it doesn't have a name.
The closure type has operator() defined as expected, taking the parameter list and returning what the body returns. For a non-capturing lambda, the closure type has no member variables, so its objects are essentially stateless. Capturing lambdas store their captured values as member variables of the closure type.
One more thing about the closure type: by default, operator() is const. The body can read captured values but not modify them. The mutable keyword lifts that restriction.
The diagram walks through what happens when the compiler sees a lambda. It generates a private class with a single operator() whose signature matches the lambda's parameter list, then the lambda expression itself is an object of that class. There's no runtime magic, no hidden vtable, no extra indirection. Calling cmp(1, 2) is a direct call to __Lambda_xyz::operator()(1, 2).
Captures deserve their own chapter, and they get one later. A working idea of them is needed to follow the rest of this chapter, because real lambdas almost always need to see at least one variable from the enclosing scope.
Two basic capture forms cover most cases:
Capture by value: [name] makes a copy of name into the closure object. The copy is independent of the outside variable from that point on.
The lambda captured cutoff as 20.0. Even though cutoff was later reassigned to 100.0, the lambda still uses its own copy of 20.0. Capture by value snapshots the value at the moment the lambda expression runs.
Capture by reference: [&name] stores a reference to name, not a copy. Changes to the outside variable are visible inside the lambda, and changes the lambda makes (if it's mutable or if name itself isn't const) are visible outside.
The lambda holds a reference to orderCount and increments the actual variable in main. After three calls, orderCount is 3.
That's all this chapter covers about captures. The rest, capturing multiple variables, default captures ([=] and [&]), this, mutable, init captures, generic lambdas, lives in the Lambda Captures & Generic Lambdas lesson. The takeaway: [var] is by value, [&var] is by reference, and lambdas have full access to whatever they capture.
Capture by value copies the variable into the closure object. For a std::string or a large struct, that's a real allocation or memory copy at the point the lambda is constructed. Capture by reference is free at construction time but creates a lifetime dependency: the lambda must not outlive what it references.
A lambda with an empty capture list (no [name], no [&], no [=], just []) has a special property: it implicitly converts to a plain function pointer of matching signature. This is convenient when interoperating with C APIs or older C++ code that takes function pointers.
The lambda is converted to a plain function pointer because it has no state to carry. The closure type is empty, so the compiler can produce a regular function with the same signature and return a pointer to it.
This conversion only works for non-capturing lambdas. A lambda that captures anything by value or by reference has state inside the closure object, and a plain function pointer can't carry state. The compiler refuses:
Broken code:
The lambda captures discount, so the closure type has a member variable. A function pointer is just an address; it has nowhere to store discount. The compiler reports something like:
The fix is either to drop the capture (and pass discount as a parameter) or to store the lambda in something that can hold state, like auto or std::function. Those are covered next.
The standard library was designed around callable objects, and lambdas are now the everyday way to hand behavior to algorithms. The pattern is almost always: pick an algorithm from <algorithm>, pass a range (begin, end), and pass a lambda that says what to do.
std::sort with a custom comparatorA bookstore lists books with a title and a rating. To sort by rating from highest to lowest:
The comparator returns true when the first argument should come before the second. For descending order by rating, that means "a comes first if its rating is greater than b's". The lambda is the one piece of code specific to "sort books by rating descending", and it lives right at the call site.
std::sort is O(n log n) and typically uses introsort (quicksort with a heap-sort fallback). The comparator runs O(n log n) times, so an expensive comparator dominates the cost. Keep comparator lambdas cheap and avoid recomputing values inside them.
std::for_each to apply a side effectstd::for_each runs a callable on every element of a range. It's most useful when the action has a side effect, like printing, logging, or updating an external counter.
The lambda captures total by reference and adds each price to it. By the end of the call, total holds the sum. std::accumulate also works for sums specifically, but for_each works for any side effect, not just accumulation.
In modern code, a range-based for loop is often clearer than std::for_each for the same task. The algorithm version is useful when composing it with other algorithms or when making the iteration explicit at the type level.
std::find_if to locate the first matchstd::find_if walks the range and returns an iterator to the first element for which the lambda returns true. If nothing matches, it returns end().
The lambda is a predicate: it takes one argument and returns bool. find_if is one of a family that includes std::any_of, std::all_of, std::none_of, std::count_if, and std::remove_if. All of them take the same kind of predicate lambda.
A capturing lambda provides a parametrized predicate. To find the first product under a threshold price:
budget is captured by value, so the lambda carries its own copy of 20.0. Changing budget to a different number and recreating the lambda produces a different predicate. This is the lambda pattern that's hardest to replicate cleanly with a free function: a free function has nowhere to put budget, so it would have to be passed as an extra parameter (which doesn't fit find_if's expected signature) or replaced with a functor that has a constructor.
auto vs std::functionOnce a lambda is constructed, it needs a place to live. There are three main options.
The simplest case: don't store the lambda at all, pass it directly into the function that needs it. This is what every std::sort example so far has done.
No variable, no type name, no storage decision. For a lambda used once and never named, inline is the right answer.
autoFor a lambda used more than once, give it a name with auto:
The variable has the lambda's actual closure type, which means the compiler knows exactly what's being called. It can inline the body at every call site, which makes the call as cheap as a regular function call. There's no runtime indirection, no type erasure, no allocation.
The downside is that the type can't be written by hand. The closure type has no name. So storing the lambda as a class member, in a std::vector, or returning it from a function, auto (or decltype) only takes things so far. Member variables can't be declared auto (until C++17 with auto non-type template parameters and other narrow cases that don't apply here). Vectors need a single concrete element type, but each lambda has its own type. Return values can use auto since C++14, which is fine.
std::functionstd::function<R(Args...)> from <functional> is a type-erased wrapper. It can hold any callable with the matching signature: a function pointer, a lambda, a functor, a std::bind result. The price is that calling through a std::function has runtime overhead, and constructing one from a stateful lambda may allocate.
Each lambda has its own closure type. They're all incompatible with each other and can't share an auto slot. std::function<double(double)> is a single type that erases their differences, so all three fit in one vector.
The cost is real. A std::function call typically goes through a virtual dispatch or a function-pointer indirection internally, which the compiler can't inline. For a lambda small enough to fit in std::function's small-buffer optimization (usually around 16-32 bytes, implementation-defined), there's no heap allocation, just an internal copy. For larger lambdas (lots of captures, large captured types), std::function allocates on the heap to store the closure.
std::function calls are typically 5-20 nanoseconds slower than a direct call, because the compiler can't inline through the type-erased wrapper. Construction may allocate. Use it for polymorphism over callables. Use auto (or a template) otherwise.
| Use case | Prefer | Why |
|---|---|---|
| One-off comparator/predicate passed to an algorithm | Inline lambda | No naming overhead, easiest to read |
| Lambda used at multiple call sites in one scope | auto | Zero runtime cost, compiler inlines |
| Storing lambdas in a container (vector, map) | std::function | Different lambdas have different types; need erasure |
| Class member that holds a callback | std::function | Members can't be auto; need a writable type |
| Function parameter for a callable | Template (template<class F>) | Zero cost, accepts any callable |
| Function parameter when you need a uniform type | std::function | Type erasure at the parameter |
The default is auto. Use std::function only when required, usually for a concrete, writable type for storage or for a class member.
Pulling the pieces together: an online store wants to list all in-stock products under a budget, sorted from cheapest to most expensive, with a 10% discount applied. Three lambdas, three algorithms.
Three different storage choices in one program. The predicate is auto because it's used once but benefits from being named for clarity. The comparator is auto because it has no state. The discount is std::function because a real store would swap it for a different rule (a $5-off lambda, a holiday-only lambda) at runtime, and the storage has to accept any of them.
std::copy_if walks the catalog and copies every product matching the predicate into matches. std::sort puts them in ascending price order. The final loop applies the discount through the std::function wrapper. All three callables are lambdas, all three live at the call site or one line above it.
The diagram shows the data flowing through three algorithm calls, each with its own lambda. The catalog (cyan) enters, the predicate lambda inside copy_if (orange) filters it down to a smaller list of matches (green), the comparator lambda inside sort reorders them, and the discount lambda inside the print loop transforms each price for display. This is the everyday shape of code that uses the standard library: data plus a pipeline of algorithms, each parametrized by a small lambda.
10 quizzes