Table of Contents

Let’s start the week with Lambda Expressions. The plan is to have a set of concise articles presenting core elements of lambda expressions. Today you can see how the syntax has evolved starting since C++11 and what are the latest changes in C++20.

The Series

This blog post is a part of the series on lambdas:

  • The syntax changes (Tuesday 4th August) (this post)
  • Capturing things (Wednesday 5th August)
  • Going generic (Thursday 6th August)
  • Tricks (Friday 5th August)

Syntax in C++11

The first iteration of lambdas!

In a basic form they have the following syntax:

[]() specifiers exception attr -> ret { /*code; */ }
  1. [] - introduces the lambda expression, capture clause
  2. () - the list of arguments, like in a regular function, optional if specifiers/exception list is empty
  3. specifiers/exception/attr - mutable, noexcept - additional specifiers
  4. ret - trailing return type, in most cases not needed as the compiler can deduce the type
  5. /* code; */ - the body of the lambda

You can read the spec located under N3337 - the final draft of C++11: [expr.prim.lambda].

Some example:

// 1. the simplest lambda:
[]{};

// 2. with two params:
[](float f, int a) { return a * f; };
[](int a, int b) { return a < b; };

// 3. trailing return type:
[](MyClass t) -> int { auto a = t.compute(); print(a); return a; };

// 4. additional specifiers:
[x](int a, int b) mutable { ++x; return a < b; };
[](float param) noexcept { return param*param; };
[x](int a, int b) mutable noexcept { ++x; return a < b; };

// 5. optional ()
[x] { std::cout << x; }; // no () needed
[x] mutable { ++x; };    // won't compile!
[x]() mutable { ++x; };  // fine - () required before mutable
[] noexcept { };        // won't compile!
[]() noexcept { };      // fine

Syntax in C++14

In C++14 the “high level” syntax hasn’t changed much, but the capture clause allows you perform “capture with initialiser”, and the parameter list can take auto arguments (it means generic lambdas).

Additionally, the return type of a lambda expression follows the rules of a regular function return type deduction (auto), so in short, compilers are smarter now.

You can see the specification in N4140 and lambdas: [expr.prim.lambda].

Some examples:

The first one with a capture with an initialiser:

#include <iostream>

int main() {
    int x = 30;
    int y = 12;
    const auto foo = [z = x + y]() { std::cout << z << '\n'; };
    x = 0;
    y = 0;
    foo();
}

As you can see above the compiler can now create member variables for closure type from expressions like z = x + y.

And another significant change is a generic lambda which supports auto as an argument.

const auto foo = [](auto x, auto y) { /*...*/ };

Syntax in C++17

Since C++17 you can now use constexpr as an additional specifier for the lambda.

[]() specifiers exception attr -> ret { /*code; */ }
  1. [] - introduces the lambda expression, capture clause
  2. () - the list of arguments, like in a regular function, optional if specifiers/exception list is empty
  3. specifiers/exception/attr - mutable, noexcept, constexpr
  4. ret - trailing return type
  5. /* code; */ - the body of the lambda

Some example:

constexpr auto Square = [](int n) { return n * n; }; // implicit constexpr
static_assert(Square(2) == 4);

And additionally the capture syntax supports*this:

struct Baz {
    auto foo() {
        return [*this] { std::cout << s << std::endl; };
    }
    
    std::string s;
};

Syntax in C++20

Since C++17 you can now use consteval as an additional specifier for the lambda, and what’s more, you can pass a template tail!

[]<tparams>() specifiers exception attr -> ret requires { /*code; */ }
  1. [] - introduces the lambda expression, capture clause
  2. <tparams> - template tail, template arguments
  3. () - the list of arguments, like in a regular function, optional if specifiers/exception list is empty
  4. specifiers/exception/attr - mutable, noexcept, constexpr, consteval
  5. ret - trailing return type
  6. /* code; */ - the body of the lambda

Some examples:

int main() {
    const int x = 10;
    auto lam = [](int x) consteval { return x + x; };
    return lam(x);
}

Template lambdas and perfect forwarding:

auto ForwardToTestFunc = []<typename ...T>(T&& ...args) {
  return TestFunc(std::forward<T>(args)...);
};

Next Time

In the next article, you’ll see how to capture things from the external scope. See here:

See More in Lambda Story

If you like to know more, you can see my book on Lambdas! Here are the options on how to get it and join 1000 of readers: