Jonathan Boccara's blog

The Evolutions of Lambdas in C++14, C++17 and C++20

Published December 13, 2021 - 0 Comments

Lambdas are one of the most popular features of Modern C++. Since their introduction in C++11, they’ve become ubiquitous in C++ code.

But since their appearance in C++11, they have evolved and gained significant features. Some of those features help write more expressive code, and since using lambdas is so common now, it is worth it to spend time learning what we can do with them.

Our goal here is to cover the major evolutions of lambdas, but not all the little details. A comprehensive coverage of lambdas would be more suited for a book than an article. If you want to dig more, I recommend Bartek’s book C++ Lambda Story, that will tell you everything.

The general evolution of lambdas is to give them the capabilities of function objects manually defined.

This articles assumes you know the basics of lambdas in C++11. Let’s take it from C++14.

Lambdas in C++14

In C++14, lambdas get 4 major enhancements:

  • default parameters
  • template parameters
  • generalised capture
  • returning a lambda from a function

Default parameters

In C++14, lambdas can take default parameters, like any function:

auto myLambda = [](int x, int y = 0){ std::cout << x << '-' << y << '\n'; };

std::cout << myLambda(1, 2) << '\n';
std::cout << myLambda(1) << '\n';

This code outputs this:

1-2
1-0

Template parameters

In C++11 we have to define the type of the parameters of lambdas:

auto myLambda = [](int x){ std::cout << x << '\n'; };

In C++14 we can make them accept any type:

auto myLambda = [](auto&& x){ std::cout << x << '\n'; };

Even if you don’t need to handle several types, this can be useful to avoid repetition and make the code more compact and readable. For example this kind of lambda:

auto myLambda = [](namespace1::namespace2::namespace3::ACertainTypeOfWidget const& widget) { std::cout << widget.value() << '\n'; };

becomes that:

auto myLambda = [](auto&& widget) { std::cout << widget.value() << '\n'; };

Generalised capture

In C++11, lambdas can only capture existing objects in their scope:

int z = 42;
auto myLambda = [z](int x){ std::cout << x << '-' << z + 2 << '\n'; };

But with the powerful generalised lambda capture, we can initialise captured values with about anything. Here is a simple example:

int z = 42;
auto myLambda = [y = z + 2](int x){ std::cout << x << '-' << y << '\n'; };

myLambda(1);

This code outputs this:

1-44

Returning a lambda from a function

Lambdas benefit from a language feature of C++14: returning auto from a function, without specifying the return type. Since the type of a lambda is generated by the compiler, in C++11 we could not return a lambda from a function:

/* what type should we write here ?? */ f()
{
    return [](int x){ return x * 2; };
}

In C++14 we can return a lambda by using auto as a return type. This is useful in the case of a big lambda sitting in the middle of a piece of code:

void f()
{
    // ...
    int z = 42;
    auto myLambda = [z](int x)
                    {
                        // ...
                        // ...
                        // ...
                    };
    // ...
}

We can pack away the lambda in another function, thus introducing another level of abstraction:

auto getMyLambda(int z)
{
    return [z](int x)
           {
               // ...
               // ...
               // ...
           };
}

void f()
{
    // ...
    int z = 42;
    auto myLambda = getMyLambda(z);
    // ...
}

To know more about this technique, explore the fascinating topic of out-of-line lambdas.

Lambdas in C++17

C++17 brought one major enhancement to lambdas: they can be declared constexpr:

constexpr auto times2 = [] (int n) { return n * 2; };

Such lambdas can then be used in contexts evaluated at compile time:

static_assert(times2(3) == 6);

This is particularly useful in template programming.

Note though that constexpr lambdas become much more useful in C++20. Indeed, it is only in C++20 that std::vector and most STL algorithms become constexpr too, and they can be used with constexpr lambdas to create elaborate manipulations of collections evaluated at compile time.

There is an exception one container though: std::array.  The non-mutating access operations of std::array become constexpr as soon as C++14 and the mutating ones become constexpr in C++17.

Capturing a copy of *this

Another feature that lambdas got in C++17 is a simple syntax to capture a copy of *this. To illustrate, consider the following example:

struct MyType{
    int m_value;
    auto getLambda()
    {
        return [this](){ return m_value; };
    }
};

This lambda captures a copy of this, the pointer. This can lead to memory errors if the lambda outlives the object, for example in the following example:

auto lambda = MyType{42}.getLambda();
lambda();

Since MyType is destroyed at the end of the first statement, calling lambda on the second statement dereferences this to access its m_value, but this points to a destroyed object. This leads to undefined behaviour, typically a crash of the application.

One possible way to solve that is to capture a copy of the whole object inside of the lambda. C++17 provides the following syntax to achieve that (note the * before this):

struct MyType
{
    int m_value;
    auto getLambda()
    {
        return [*this](){ return m_value; };
    }
};

Note that it was already possible to achieve the same result in C++14 with generalised capture:

struct MyType
{
    int m_value;
    auto getLambda()
    {
        return [self = *this](){ return self.m_value; };
    }
};

C++17 only makes the syntax nicer.

Lambdas in C++20

Lambdas evolved in C++20, but with features arguably less fundamental than those of C++14 or C++17.

One enhancement of lambdas in C++20, that brings them even closer to manually defined function objects, is the classic syntax to define templates:

auto myLambda = []<typename T>(T&& value){ std::cout << value << '\n'; };

This makes it easier to access the template parameter type than the C++14 template lambdas that used expressions such as auto&&.

Another improvement is to be able to capture a variadic pack of parameters:

template<typename... Ts>
void f(Ts&&... args)
{
    auto myLambda = [...args = std::forward<Ts>(args)](){};
}

Dive into lambdas

We’ve been over what I consider being the major improvements of lambdas from C++14 to C++20. But there is more to it. Those major features come along with quite a few little things that make lambda code simpler to write.

Diving into lambdas is a great opportunity to get a better understanding of the C++ language, and I think it’s a worthwhile investment of time. To go further, the best resource I know of is Bartek’s C++ Lambda Story book, which I recommend.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin