0%

Lambdas in C++: Function Objects in Disguise

This is my learning notes from the course - Mastering C++ Standard Library Features.

Lambdas: Function Objects in Disguise

  • How lambda expressions work
  • Desugaring lambdas to function objects
  • Performance overhead of lambda expressions

Lambdas are just “syntactic sugar” for function objects.

Example: Printing Elements of an std::vector

  • C++03:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct printer
    {
    // overload function call operator()
    void operator()(int x) const
    {
    std::cout << x;
    }
    };

    std::vector<int> v{1, 2, 3, 4, 5};
    std::for_each(v.begin(), v.end(), printer());

    *”The function call operator () can be overloaded for objects of class type. When you overload ( ), you are not creating a new way to call a function. Rather, you are creating an operator function that can be passed an arbitrary number of parameters.”
    – from Function Call Operator () Overloading in C++*

  • C++11:

    1
    2
    3
    4
    5
    std::vector<int> v{1, 2, 3, 4, 5};
    std::for_each(v.begin(), v.end(), [](int x)
    {
    std::cout << x;
    });

    Desugaring: Simple Lambda

    1
    [](int x){ std::cout << x; }

    is equivalent to

    1
    2
    3
    4
    struct anonymous_lambda_type
    {
    auto operator()(int x) const { std::cout << x; }
    };
    • Notice that, the const qualifier on operator() is implicit.
      • The rules follow auto type deduction. It copies by default, so be careful!

Desugaring: Lambda with Return Statement

Lambda Captures

  • Lambdas can capture variables from the surrounding environment.

    1
    2
    3
    4
    std::vector<int> v{1, 2, 3, 4, 5};
    auto& os = std::cout;

    std::for_each(v.begin(), v.end(), [&os](int x){ os << x; });

    Desugaring: Lambda with Capture (by Reference)

    1
    [&os](int x){ os << x }

    is equivalent to

    1
    2
    3
    4
    5
    6
    7
    struct anonymous_lambda_type
    {
    std::ostream& d_os;
    anonymous_lambda_type(std::ostream& os) : d_os(os) { }

    auto operator()(int x) const { d_os << x; }
    };
    • The struct and its constructor are implicitly generated by the compiler for you.

Variables can be captured either by copy or by reference.

Desugaring: Lambda with Capture (by Copy)

1
[i]{ std::cout << i; }

is equivalent to

1
2
3
4
5
6
7
struct anonymous_lambda_type
{
int d_i;
anonymous_lambda_type(int i) : d_i(i) { }

auto operator()() const { std::cout << d_i; }
};

Desugaring: Lambda with Capture (by Reference)

1
[&i]{ std::cout << i; }

is equivalent to

1
2
3
4
5
6
7
struct anonymous_lambda_type
{
int& d_i;
anonymous_lambda_type(int& i) : d_i(i) { }

auto operator()() const { std::cout << d_i; }
};

Performance Overhead

  • Every lambda expression produces a unique anonymous type
  • There is no type-erasure: the compiler is able to inline aggressively
  • Lambda expressions are zero-overhead abstractions
  • https://godbolt.org/g/MTX3mW

Summary

  • Learned that the Lambda expressions are syntactic sugar for function objects
  • The generated function object type is “anonymous”
  • Found that it contains a const qualified operator() that returns auto
  • Studied that the captures are stored inside the closure
  • Learned that there are member variables of the generated function object type