0%

Anatomy of a Lambda in C++

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
2
3
4
[=, &b, &c](int a, float b) mutable -> int
{
return a + b;
}
  • [ /* */ ] 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
2
3
4
struct anonymous_lambda_type
{
auto operator()() { }
};

Another example:

1
2
3
4
5
6
7
8
int i = 0;
auto l = [i]() mutable { return i++; };

assert( l() == 0 );
assert( l() == 1 );
assert( l() == 2 );
assert( l() == 3 );
assert( l() == 4 );
  • 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-list

    1
    2
    3
    4
    5
    6
    7
    8
    const 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
    8
    struct 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 the auto is a typename generated by the compiler.

  • More auto parameters desugar to multiple template parameters

    1
    const auto l = [](auto a, auto b, auto c) { };

    is equivalent to

    1
    2
    3
    4
    5
    6
    7
    struct 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
    4
    const auto l = [](auto&& x)  // forwarding reference, not rvalue reference
    {
    sink(std::forward<decltype>(x)>(x));
    };
  • Example: Variadic generic lambdas

    1
    2
    3
    4
    const auto log_error = [](auto ... xs)
    {
    log(severity::error, xs ... );
    };
  • Example: Passing lambdas to lambdas

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const 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]{ } - capture a by copy
  • [ &a]{ } - capture a by reference
  • [ &, a]{ } - capture a by copy, everything else by reference
  • [ =, &a]{ } - capture a 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!)
  • [ ] ( ) int& { /* */ }
    • Takes nothing, returns int&
  • [ ] ( float) mutable char { /* */ }
    • Takes a float, returns a char, has non-const operator()

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
    7
    struct anonymous
    {
    int i = 0;
    auto operator()() const { }
    };

    anonymous lambda;
  • Example:

    1
    2
    int j;
    auto lambda = [i = j]{ };

    is equivalent to

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct 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
    7
    auto 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 move

    1
    2
    auto up = std::make_unique<foo>();
    auto l = [up = std::move(up)]{ };
  • Example: Moving capture from closure body

    1
    2
    3
    4
    5
    auto 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
    13
    struct 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 into sink 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 the sink 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
2
3
4
struct anonymous
{
constexpr auto operator()() { return 10; }
};

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 the const qualifier from the closure’s generated operator()
  • 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