Lambda Expressions
6.1 Lambda Fundamentals
1) Lambda Expression
Lambda Expression: An expression that creates a function object.
std::find_if(
container.begin(),
container.end(),
[](int val) { return 0 < val && val < 10; }
);
Lambdas provide a concise way to define small functions directly where they are used.
2) Closure
Closure: The runtime object created from a lambda expression.
A closure stores copies of or references to captured variables.
3) Closure Class
Closure Class: The compiler-generated class used to create closures.
Each lambda expression generates a unique closure class.
6.2 Avoid Default Capture Modes
1) Lambda Capture
Lambda Capture: A mechanism that allows a lambda to access variables from its surrounding scope.
[x] // capture x by value
[&x] // capture x by reference
Explicit captures make the lambda's dependencies visible.
2) Default Capture
C++ provides two default capture modes.
[=] // default by-value capture
[&] // default by-reference capture
Default captures should generally be avoided.
3) Reference Capture
Capturing by reference can create dangling references if the closure outlives the captured object.
filters.emplace_back(
[&divisor](int value)
{
return value % divisor == 0;
}
);
The lifetime of every referenced object must exceed the lifetime of the closure.
4) Value Capture
Capturing by value copies local variables into the closure.
[divisor](int value)
{
return value % divisor == 0;
}
This avoids dangling references to the original local variable.
5) Capturing this
Data members cannot be captured directly because they are not local variables.
class Widget {
int divisor;
void addFilter() const
{
filters.emplace_back(
[=](int value)
{
return value % divisor == 0;
}
);
}
};
Here, [=] captures the this pointer rather than divisor.
Therefore, the closure still depends on the lifetime of the original object.
6) Static Variables
Variables with static storage duration are not captured.
static int divisor = 5;
[=](int value)
{
return value % divisor == 0;
}
The lambda accesses the existing static object directly.
6.3 Use Init Capture to Move Objects into Closures
1) Init Capture
Init Capture: A C++14 capture mechanism that creates and initializes a data member inside the closure.
[x = expression]
It is also called generalized lambda capture.
2) Move Capture
Init capture allows move-only objects to be moved into closures.
auto pw = std::make_unique<Widget>();
auto func =
[pw = std::move(pw)]
{
return pw->isValidated();
};
This is useful for types such as std::unique_ptr.
3) Direct Initialization
The captured object can be initialized directly from an expression.
auto func =
[pw = std::make_unique<Widget>()]
{
return pw->isValidated();
};
The captured variable does not need to exist before the lambda.
4) C++11 Limitation
C++11 does not support init capture.
Move capture can instead be approximated using:
- A manually written function object
std::bind
C++14 init capture should be preferred when available.
6.4 Forward auto&& Parameters with decltype
1) Generic Lambda
Generic Lambda: A C++14 lambda whose parameters use auto.
auto f =
[](auto x)
{
return func(x);
};
The closure's operator() becomes a function template.
2) Universal Reference Parameter
Using auto&& creates a universal reference parameter.
auto f =
[](auto&& x)
{
// ...
};
It can bind to both lvalues and rvalues.
3) Perfect Forwarding
A named parameter is always an lvalue expression, so forwarding requires std::forward.
auto f =
[](auto&& x)
{
return func(
std::forward<decltype(x)>(x)
);
};
decltype(x) preserves the reference type needed by std::forward.
4) Variadic Generic Lambda
The same rule applies to multiple parameters.
auto f =
[](auto&&... params)
{
return func(
std::forward<decltype(params)>(params)...
);
};
Use decltype on auto&& parameters when perfect forwarding them.
6.5 Prefer Lambdas to std::bind
1) std::bind
std::bind: A utility that creates a function object by binding arguments to another callable object.
using namespace std::placeholders;
auto f = std::bind(func, _1, 10);
Placeholders such as _1 represent arguments supplied later.
2) Readability
Lambdas usually express the intended operation more directly.
auto f =
[](int x)
{
return func(x, 10);
};
The relationship between parameters and function arguments is immediately visible.
3) Evaluation Time
Arguments passed to std::bind are normally evaluated when the bind object is created.
Lambda expressions make it easier to control when expressions are evaluated.
4) Overloaded Functions
std::bind can have difficulty resolving overloaded function names.
void setAlarm(Time, Sound, Duration);
void setAlarm(Time, Sound, Duration, Volume);
A function pointer cast may be required to select the intended overload.
Normal calls inside lambdas use ordinary overload resolution.
5) Capture Semantics
Lambda capture syntax explicitly shows whether an object is stored by value or reference.
[w] // value
[&w] // reference
With std::bind, argument-storage behavior is less obvious from the call site.
6) Performance
Calls written inside lambdas are ordinary function calls and can be easier for compilers to inline.
Calls through std::bind may involve indirect function calls and can be harder to optimize.
7) C++11 Exceptions
In C++11, std::bind can still be useful for:
- Emulating move capture
- Binding objects with templated function-call operators
C++14 generic lambdas and init capture largely eliminate these use cases.
Core Rules
-
Distinguish lambda expression, closure, and closure class.
-
Prefer explicit captures such as
[x]and[&x]over default captures[=]and[&]. -
Reference captures require careful lifetime management.
-
[=]inside a member function may capturethis, not individual data members. -
Use C++14 init capture to move objects into closures.
[p = std::move(p)]
- A generic lambda uses
autoparameters.
[](auto x) { ... }
- Use
auto&&withstd::forward<decltype(param)>for perfect forwarding.
[](auto&& param)
{
return f(
std::forward<decltype(param)>(param)
);
}
- Prefer lambdas to
std::bindbecause they are generally more readable, expressive, and potentially easier to optimize.