This is my learning notes from the course - Mastering C++ Standard Library Features.
Anatomy of a Lambda in C++
- Lambda expression syntax
Mutablelambdas- Generic lambdas
- Generalized lambda captures
Constexprlambdas
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
mutableand/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; |
Mutableallows captured objects to be mutated- Note that the copy of
iis being mutated, not the original one
Generic Lambdas (C++14)
In C++14,
autocan 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
autoas part of the parameter list in the lambda generated a templated operator call, where theautois a typename generated by the compiler.More
autoparameters 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]{ }- captureaby copy[ &a]{ }- captureaby reference[ &, a]{ }- captureaby copy, everything else by reference[ =, &a]{ }- captureaby 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_ptrby 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
upunique_ptr is a data member of the generated closure struct. If we try to move it intosinkfrom 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::moveon a data member will produce a non-const rvalue reference, which can be moved from thesinkfunction.
Constexpr Lambdas (C++17)
- In C++17, a lambda expression is implicitly
constexprif 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
mutablecan be used to remove theconstqualifier from the closure’s generatedoperator() - Learned that auto parameters can be used to generated closures with a templated
operator() - Lambdas can be
constexprin C++17
Guidelines
- Use “generalized lambda captures“ to move objects inside closures
- Use
mutablewhen moving objects from closures - Explicitly defined your captures for readability, unless obvious
- Use
generic lambdasto 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