This is my learning notes from the course - Mastering C++ Standard Library Features.
Anatomy of a Lambda in C++
- Lambda expression syntax
Mutable
lambdas- Generic lambdas
- Generalized lambda captures
Constexpr
lambdas
Lambda Syntax Overview
1 | [=, &b, &c](int a, float b) mutable -> int |
[ /* */ ]
is the capture-list- The capture-list is followed by the parameter-list:
( /* */ )
- The parameter-list is optionally followed by
mutable
and/or a trailing return type - The body of the lambda is always at the end
Mutable Lambdas
Mutable
removes the const
qualifier from the operator()
1 | []() mutable { } |
is equivalent to
1 | struct anonymous_lambda_type |
Another example:
1 | int i = 0; |
Mutable
allows captured objects to be mutated- Note that the copy of
i
is being mutated, not the original one
Generic Lambdas (C++14)
In C++14,
auto
can be used in the parameter-list1
2
3
4
5
6
7
8const auto l = [](const auto& x)
{
std::cout << x;
};
l("hello!");
l(1);
l(foo{});The above code
1
const auto l = [](const auto& x) { std::cout << x; };
is equivalent to
1
2
3
4
5
6
7
8struct anonymous
{
template <typename T>
auto operator()(const T& x) const
{
std::cout << x;
}
};In this case, putting
auto
as part of the parameter list in the lambda generated a templated operator call, where theauto
is a typename generated by the compiler.More
auto
parameters desugar to multiple template parameters1
const auto l = [](auto a, auto b, auto c) { };
is equivalent to
1
2
3
4
5
6
7struct anonymous
{
template <typename T0, typename T1, typename T2>
auto operator()(T0 a, T1, b, T2 c) const
{
}
};
Generic lambdas are very powerful because they allow you to take any type as an argument to the lambda without requiring type erasure or any performance overhead.
This can allow you to create very neat interfaces or short functions that work with multiple types.
Some Examples of Generic Lambdas (C++14)
Example: Forwarding reference
1
2
3
4const auto l = [](auto&& x) // forwarding reference, not rvalue reference
{
sink(std::forward<decltype>(x)>(x));
};Example: Variadic generic lambdas
1
2
3
4const auto log_error = [](auto ... xs)
{
log(severity::error, xs ... );
};Example: Passing lambdas to lambdas
1
2
3
4
5
6
7
8
9
10
11const auto call_twice = [](auto f)
{
f(); f();
};
const auto print_hello_world = []
{
std::cout << "hello world!\n";
};
call_twice(print_hello_world);
Capture List Syntax
[ ]{ }
- no captures[ =]{ }
- capture everything by copy (be careful!)[ &]{ }
- capture everything by reference (be careful!)[ a]{ }
- capturea
by copy[ &a]{ }
- capturea
by reference[ &, a]{ }
- capturea
by copy, everything else by reference[ =, &a]{ }
- capturea
by reference, everything else by copy- Captured variables are “stored“ in the lambda instance
Lambda Signature Examples
[ ] ( int){ /* */ }
- Takes an
int
, deduces return type (by value!)
- Takes an
[ ] ( ) int& { /* */ }
- Takes nothing, returns
int&
- Takes nothing, returns
[ ] ( float) mutable char { /* */ }
- Takes a
float
, returns achar
, hasnon-const operator()
- Takes a
Generalized Lambda Capture (C++14)
- Allow to specify the name of a data member in the closure generated by the lambda expression, and to initialize it with an arbitrary expression
- Examples:
[ i = 0 ] { }
[ x = std::move(foo) ] { }
[ a{10}, b{15} ] { }
More Examples
Example:
1
auto lambda = [i = 0]{ };
is equivalent to
1
2
3
4
5
6
7struct anonymous
{
int i = 0;
auto operator()() const { }
};
anonymous lambda;Example:
1
2int j;
auto lambda = [i = j]{ };is equivalent to
1
2
3
4
5
6
7
8
9struct anonymous
{
int i;
anonymous(int j) : i{j} { }
auto operator()() const { }
};
int j;
anonymous lambda{j};Example: Integer sequence generator
1
2
3
4
5
6
7auto l = [i = 0]() mutable { return i++; }
assert( l() == 0 );
assert( l() == 1 );
assert( l() == 2 );
assert( l() == 3 );
assert( l() == 4 );Example: Capturing
unique_ptr
by move1
2auto up = std::make_unique<foo>();
auto l = [up = std::move(up)]{ };Example: Moving capture from closure body
1
2
3
4
5auto up = std::make_unique<foo>();
auto l = [up = std::move(up)]() mutable
{
sink(std::move(up));
};Example: With/without mutable
1
2
3
4
5
6
7
8
9
10
11
12
13struct without_mutable
{
std::unique_ptr<foo> up;
// ... constructor ...
auto operator()() const { sink(std::move(this->up)); }
};
struct with_mutable
{
std::unique_ptr<foo> up;
// ... constructor ...
auto operator()() { sink(std::move(this->up)); }
};As you can see, the
up
unique_ptr is a data member of the generated closure struct. If we try to move it intosink
from the version without mutable, we will attempt to move from a const reference to a unique_ptr, which cannot produce a non-const lvalue reference and thus we cannot move from it.Instead, in the mutable case, since we don’t have the const qualifier,
std::move
on a data member will produce a non-const rvalue reference, which can be moved from thesink
function.
Constexpr Lambdas (C++17)
- In C++17, a lambda expression is implicitly
constexpr
if possible - It can also be explicitly marked as such
1 | []{ return 10; } // implicitly `constexpr` |
… is equivalent to …
1 | []() constexpr { return 10; } |
If you’ve never seen the constexpr
keyword before, all you need to know to understand constexpr
lambdas is that the result of these lambda expressions is available at compile time and can be used in constexpr
expressions, such as the size of an array.
This means that you can use the lambda that returns 10 to maybe get the size of an array or fill in a non-type template parameter, which needs to be known at compile time.
Let’s see how these lambdas expend.
1 | []{ return 10; } // implicitly `constexpr` |
… and …
1 | []() constexpr { return 10; } |
… are equivalent to …
1 | struct anonymous |
As you can see in the example code, the operator call can be invoked and its result can be known at compile time.
Example
Here’s an example of something that is valid in C++17, but is a compile-time error in C++11 and C++14.
- Example: Using lambda invocation in constant expression
1
std::array<int, []{ return 10; }()> ints;
- Valid in C++17
- Compile-time error in C++11/14
Summary
- Learned that lambdas can flexibly capture the surrounding environment
- Studied that new closure data members can be defined with “generalized lambda captures”
- Discussed that
mutable
can be used to remove theconst
qualifier from the closure’s generatedoperator()
- Learned that auto parameters can be used to generated closures with a templated
operator()
- Lambdas can be
constexpr
in C++17
Guidelines
- Use “generalized lambda captures“ to move objects inside closures
- Use
mutable
when moving objects from closures - Explicitly defined your captures for readability, unless obvious
- Use
generic lambdas
to create small functions that work on multiple types or to accept other lambdas as arguments
Section Summary
- Learned that a lambda expression is syntactic sugar that produces an anonymous closure type
- Understood that lambda expressions can capture the surrounding environment either by value or by reference
- Studied that the closure type is a class with an overloaded
operator()
and data members corresponding to lambda captures - Understood that lambda expression has no inherent performance overhead
- In C++14, generalized lambda captures can be used to create arbitrary data members in the closure
- In C++17, lambdas can be
constexpr