Lambda, Expressions, and between them – From 11 to 20 – Part 1

Every language has her own scary syntax, and lambda expressions got C++ one step forward at this category. Lambda expressions are like pets: Always be nice to them, and one day they might save your life.

Before we start, a first explanation for this article. This article will show the changes in lambda expressions through C++11, 14, 17 and 20. In the next article I’ll explain how does it work behind the scenes. So take a deep breath, we are about to go deeper in the Lambda Expressions ocean.


C++11

We are on the ocean surface, it seems quiet and relax.

A lambda expression is an anonymous function which able to capture variables living in the scope. Let’s start with a fast inspection of lambda expression:

int main() {
    auto sum = [](int a, int b) -> int {
        return a + b;
    };
    std::cout << sum(1, 2) << std::endl; // Prints 3
    return EXIT_SUCCESS;
}

Note: For now, we won’t dive into the actual type of sum, in order to simplify the understanding. We’ll do that in the next article. For now we’ll refer to the type as an “anonymous function”.

Syntax

To understand the syntax, we have to separate the parts:

[ /* capture */ ] ( /* params (optional) */ ) /* specifiers (optional) */ /* exceptions */ /* attributes */ -> /* return type (optional) */ {
    /* body */
};

capture – A list of local variables that the function will be able to use. Those variables can be passed by value or by reference. The capture happens once when the function is being created.

params – The function params list.

specifiers – Optional sequence of specifiers. In C++11 the only allowed specifier is mutable which mark the captured variable as changeable inside the function body.

exception – Dynamic exception specification or noexcept specifier.

return type – The function return type.

body – function body.

attributes – provides the attribute specification for the type of the function call operator of the closure type. Any attribute so specified appertains to the type of the function call operator, not the function call operator itself. (For example, the [[noreturn]], [[nodiscard]], etc. attributes cannot be used.)

The following forms of lambda expressions are legal:

[] () mutable -> void {}; // Full declaration
[] () -> void {}; // Const-lambda declaration - captured variables cannot be modified inside lambda body.
[] () {}; // Return type deduced from return statement (if not exists - void).
[] {}; // A function without arguments. Can be used if  captured section & function body are the only sections that included in the expression (no return type specification, exceptions or specifiers).

Lambda Expressions – Why?

A word of truth: There is no program in the world that can’t be written without lambda expressions, but there are some famous cases when they can definitely simplify it. Some examples:

void thread_function(int a, int b) { std::cout << a + b << std::endl; }
int main() {
    std::thread t(thread_function, 5, 3);
    return EXIT_SUCCESS;
}

In the above example we had to create a new function only to create our thread with. Lambda expressions can simplify it:

int main() {
    std::thread t([](int a, int b) {
        std::cout << a + b << std::endl;
    }, 5, 3);
    t.join();
    return EXIT_SUCCESS;
}

Let’s see another thread usage:

class my_class {
public:
    void create_thread() {
        std::thread t(my_thread_function, std::ref(*this));
        // std::thread t1(non_static_member_function); // Won't compile - the function should be static.
        t.join();
    }
private:
    int my_int;
    static void my_thread_function(my_class& mc) { // This function has to be a static function in order to used as a thread function.
        std::cout << mc.my_int << std::endl; // In order to access a class member variable, we have to accept a class reference as a function argument.
    }
    void non_static_member_function() {}
};

This time we had a little bit more overhead in order to create a thread with a class member function. Once again, lambda expressions are here to help:

class my_class {
public:
    void create_thread_2() {
        std::thread t([this]() {
            std::cout << this->my_int << std::endl;
        });
        t.join();
    }
private:
    int my_int;
};

Much more simple than before. However, assume we don’t want the definition of our thread function inside the thread creating function, we can call a class non-static member function from the lambda body:

class my_class {
public:
    void create_thread_3() {
        std::thread t([this]() {
            this->non_static_function();
        });
        t.join();
    }
private:
    int my_int;
    
    void non_static_function() const {
        std::cout << my_int << std::endl;
    }
};

Another famous usage of lambda expressions is functions that accepts another functions as arguments (std::thread is only one of them):

std::vector<int> vec = {1, 2, 3};
std::for_each(vec.begin(), vec.end(), [](int &elem) {
    elem *= elem;
});
std::for_each(vec.begin(), vec.end(), [](int &elem) {
    std::cout << elem << " ";
});
std::cout << std::endl;

Capture Section

Probably the most complex section in the syntax is the capture. Some syntax rules:

  • To capture all of the scope variables by value, use =.
  • To capture all of the scope variables by reference, use &.
int a, b;
a = 1; b = 2;
auto lambda_1 = [=] { std::cout << a << " " << b << std::endl; };
auto lambda_2 = [&] { std::cout << a << " " << b << std::endl; };
lambda_1(); // Prints: 1 2
lambda_2(); // Prints: 1 2
a = 4; b = 5;
lambda_1(); // Prints: 1 2
lambda_2(); // Prints: 4 5
  • identifier
  • pack_parameter_identifier...
  • &identifier
  • &identifier...
  • this
struct S2 { void f(int i); };
void S2::f(int i)
{
    [&]{};          // OK: by-reference capture default
    [&, i]{};       // OK: by-reference capture, except i is captured by copy
    [&, &i] {};     // Error: by-reference capture when by-reference is the default
    [&, this] {};   // OK, equivalent to [&]
    [&, this, i]{}; // OK, equivalent to [&, i]
}

An important thing to remember- the captured by value variables will be alive as long as the lambda is alive, at the next article I’ll explain how does it work behind the scenes, but for now a quick example:

int main() {
    int a = 4;
    auto increase_local_a = [a]() mutable {
        std::cout << a << std::endl;
        a++;
    };
    auto decrease_ref_a = [&a]() mutable {
        std::cout << a << std::endl;
        a--;
    };
    increase_local_a(); // Prints: 4
    decrease_ref_a(); // Prints: 4
    increase_local_a(); // Prints: 5
    decrease_ref_a(); // Prints: 3
    increase_local_a(); // Prints: 6
    decrease_ref_a(); // Prints: 2
    std::cout << a << std::endl; // Prints: 1
    return EXIT_SUCCESS;
}

C++14

A little bit deeper in the lambda-expressions ocean, we can see new forms of expressions…

The first change is the usage of auto keyword inside the lambda params:

auto lambda = [](auto a, auto b) {
    return a + b;
};
lambda(1, 2.4); // int, double
lambda(5.f, 3); // float, int

This auto keyword is now creating a new possibility for parameters pack:

// generic lambda, operator() is a template with two parameters
auto glambda = [](auto a, auto&& b) { return a < b; };
bool b = glambda(3, 3.14); // ok
 
// generic lambda, operator() is a template with one parameter
auto vglambda = [](auto printer) {
    return [=](auto&&... ts) // generic lambda, ts is a parameter pack
    { 
        printer(std::forward<decltype(ts)>(ts)...);
        return [=] { printer(ts...); }; // nullary lambda (takes no parameters)
    };
};
auto p = vglambda([](auto v1, auto v2, auto v3) { std::cout << v1 << v2 << v3; });
auto q = p(1, 'a', 3.14); // outputs 1a3.14
q();                      // outputs 1a3.14

Capture Section

  • identifier initializer
  • & identifier initializer

Those two new possibilities make it possible to pass the same variables with different names, and to initialize variables with different values:

int a = 4, k = 7;
auto lambda = [&b = a, k = a]() mutable {
    b++;
    std::cout << k << std::endl; // Prints: 4
};
lambda();
std::cout << a << std::endl; // Prints: 5

C++17

Almost at the bottom of the ocean…

A new specifier available for us here: constexpr. This is a default specifier, which means that whenever it possible (if the function fulfills constexpr function requirements), the lambda function will be a constexpr function.

Capture Section

  • *this

Capture a copy of this object.

struct my_struct {
    my_struct() {}
    my_struct(const my_struct &) { std::cout << "Copy" << std::endl; } // Called during this->func();
    void func() {
        [*this] { // call copy constructor
        };
        [this] { // doesn't call copy constructor
        };
    }
};
my_struct ms;
ms.func();

C++20

The light can barely go through the deep water around here.

Template Lambda

A lambda function can accepts <tparams> and requires:

Syntax:

[ /* capture */ ] </* tparams (can be omitted) */> ( /* params (optional) */ ) /* specifiers (optional) */ /* exceptions */ /* attributes */ -> /* return type (optional) */ /* requires (can be omitted) */ {
    /* body */
};

Examples:

// ... Outside main...
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;
// ... main ...
auto sum = [] <typename T, Numeric U> (T num1, U num2) requires ( std::is_arithmetic_v<T> ) {
    return num1 + num2;
};
std::cout << sum(3.2, 5) << std::endl;

Unlike the auto since C++14, this time we can apply restrictions with a convenient way, and even to force two generic params to be of the same type:

using namespace std::string_literals;
auto plus = [] <typename T> (T arg1, T arg2) {
    return arg1 + arg2;
};
std::cout << plus("Hello "s, "World"s) << std::endl;

Let’s have a look on a variadic template example (with fold-expressions of C++17):

// ... Outside main ...
template <typename T>
concept PlusOperator = requires(T type) {
    { type + type };
};
// ... main ...
using namespace std::string_literals;
auto plus = [] <PlusOperator T, PlusOperator ...Args> (T first, Args ...args) requires ( std::is_same_v<T, Args> && ... ) {
    return (first + ... + args);
};
std::cout << plus(1) << std::endl; // Prints: 1
std::cout << plus(1, 4, 5) << std::endl; // Prints: 10
std::cout << plus(2.3, 4.5, 5.6) << std::endl; // Prints: 12.4
std::cout << plus("Template"s, " "s, "Lambda"s, " "s, "Expression"s) << std::endl; // Prints: Template Lambda Expression

Immediate Function

Since C++20 we can use consteval specifier. consteval cannot be used in a constexpr lambda.

The default specifier is still constexpr as in C++17.

Read about constexpr & consteval.

Capture Section

  • ... identifier initializer
  • & ... identifier initializer

by-copy / by-reference capture with an initializer that is a pack expansion

struct S2 { void f(int i); };
void S2::f(int i)
{
    [=]{};          // OK: by-copy capture default
    [=, &i]{};      // OK: by-copy capture, except i is captured by reference
    [=, *this]{};   // until C++17: Error: invalid syntax
                    // since c++17: OK: captures the enclosing S2 by copy
    [=, this] {};   // until C++20: Error: this when = is the default
                    // since C++20: OK, same as [=]
}

Conclusion

I hope you enjoyed our travel in the lambda expressions ocean, and that you learned something new about lambda expressions. The next part of this article will focus on the compilation time of lambda expressions, and about the way the compiler sees those functions (Hint: It’s much more than just a function).


Special Thanks

7 thoughts on “Lambda, Expressions, and between them – From 11 to 20 – Part 1

      1. auto sum = [](int a, int b) -> int {
        return a + b;
        };

        Our variable sum holds a pointer to the anonymous function.

        Like

      2. The syntax of a lambda expression is: 1->3{4}

        1 – Lambda capture, variables you get from the outer scope without need to pass them every time.
        2 – Lambda parameters, like every function parameters.
        3 – Return value type (the narrow here doesn’t relate to pointer syntax).
        4 – Lambda body, like a regular function body. Here we can access both the captured variables and the function variables.

        Does this explanation help?

        Like

      3. auto sum = …
        I refer to the sum variable. You said that it holds a pointer. I think it doesn’t hold a pointer.

        Like

Leave a comment