본문으로 건너뛰기

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

  1. Distinguish lambda expression, closure, and closure class.

  2. Prefer explicit captures such as [x] and [&x] over default captures [=] and [&].

  3. Reference captures require careful lifetime management.

  4. [=] inside a member function may capture this, not individual data members.

  5. Use C++14 init capture to move objects into closures.

[p = std::move(p)]
  1. A generic lambda uses auto parameters.
[](auto x) { ... }
  1. Use auto&& with std::forward<decltype(param)> for perfect forwarding.
[](auto&& param)
{
return f(
std::forward<decltype(param)>(param)
);
}
  1. Prefer lambdas to std::bind because they are generally more readable, expressive, and potentially easier to optimize.