C++ Lambda Expressions

Lambdas were introduced in C++11, they provide a lightweight, compact way to define anonymous functions on-the-fly. They can capture variables from the surrounding scope, either by value or by reference.

A lambda expression, sometimes referred to as a lambda function or as a lambda, enables you to write anonymous (unnamed) functions “in place”. This is particularly useful when you want to pass an operation as an argument to an algorithm. It makes the Standard Library algorithms more usable. Variables from the local context can be “captured” and used in the function without being passed in as parameters. While a lambda doesn't have to be used with one of the Standard Library algorithms, this is a common use case.

Basic C++ Lambda Syntax

A lambda expression consists of the following: [capture list](parameter list) -> return_type { function_body } .

C++ Lambda Expression Parts:

Parts of a C++ Lambda Expression

  • [1]. Lambda introducer (capture clause)
  • [2]. Parameter list (optional)
  • [3]. Mutable specification (optional)
  • [4]. Exception specification (optional)
  • [5]. Trailing return type (optional)
  • [6]. Lambda body

Some lambdas don't require access to their local environment. Such lambdas are defined with the empty lambda introducer []. The parameter list can also be empty, so the following is a valid lambda:

  [](){std::cout << "Hello, world!" << std::endl; };

Lambda introducer [] example:

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
std::vector<int> int_vec{ 5, 4, 3, 2, 1 };

int i2{ 2 };
// Lambda  with empty capture list[], can't access i2, because it's not captured.
std::for_each(int_vec.begin(), int_vec.end(), [](int i) {std::cout << i << " "; });
std::cout << std::endl;

// To access local variables, it must be mentioned in the capture list, e.g.[i2].
std::for_each(int_vec.begin(), int_vec.end(), [i2](int i)mutable {++i2; std::cout << i * i2 << " "; });
std::cout << std::endl;

std::cout << "i2 is now: " << i2 << '\n'; // i2 is still 2, because the mutable keyword only enables the modification of the lambda's copy of the variable. 

  // To access local variables by reference, you must add an '&' before the variable mentioned in the capture list, e.g.[&i2].
std::for_each(int_vec.begin(), int_vec.end(), [&i2](int i) { ++i2; std::cout << i2 << " "; });
std::cout << std::endl;

std::cout << "i2 is now: " << i2 << '\n'; // i2 in now: 7 

return 0;
}

By placing i2in the capture list, it's made accessible from within the lambda. By not specifying otherwise, you ensure that the capture of i2is done “by value”; just as with argument passing, passing a copy is the default. If you want to capture i2 “by reference,” you can do so by adding an & before i2 in the capture list: [&i2].

Variables captured by value are constant (immutable) by default. Adding mutable after the parameter list makes them non-constant and enables the modification of the lambda's copy of the variable.

The choice between capturing by value and by reference is basically the same as the choice for function arguments. You use a reference if you need to modify the captured object or if it's large. However, for lambdas, there's the additional concern that a lambda might outlive its caller. When passing a lambda to another thread, capturing by value ([=]) is typically better, because accessing another thread’s stack through a reference or a pointer can be very disruptive (to performance or correctness), and trying to access the stack of a terminated thread can lead to hard to find errors.

Lambda Captures

The lambda captures is a comma-separated list of zero or more captures, optionally beginning with the capture-default. The capture list defines the outside variables that are accessible from within the lambda function body. The only lambda capture defaults are:

  • [=] capture all variables currently in local block scope by copy of their current values
  • [&] capture all variables currently in local block scope by reference

A lambda introducer can take several forms:

  • []: an empty capture list. This implies that no local names from the surrounding context can be used in the lambda body. For such lambda expressions, data is obtained from arguments or from nonlocal variables.
  • [&]: implicitly capture by reference. All local variables can be used and are accessed by reference.
  • [=]: implicitly capture by value. All local variables can be used. All names refer to copies of the local variables taken at the point of call of the lambda expression.
  • [capture-list]: explicit capture; the capture-list is the list of names of local variables to be captured (i.e., stored in the object) by reference or by value. Variables with names preceded by & are captured by reference. Other variables are captured by value. A capture list can also contain this and names followed by … as elements.
  • [&, capture-list]: implicitly capture by reference all local variables with names not mentioned in the list. The capture list can contain this. Listed names cannot be preceded by &. Variables named in the capture list are captured by value.
  • [=, capture-list]: implicitly capture by value of all local variables with names not mentioned in the list. The capture list cannot contain this. The listed names must be preceded by &. Variables named in the capture list are captured by reference.

Note that a local name preceded by & is always captured by reference and a local name not preceded by & is always captured by value. Only capture by reference allows modification of the referenced variables in the calling environment.

You don’t need to “capture” namespace variables (including global variables) because they are always accessible (provided they are in scope).

Lambda Parameter List

In addition to capturing variables, a lambda can accept input parameters. A lambda parameter list is optional and in most ways resembles the parameter list for a function.

auto l1 = [] (auto first, auto second)
{
return first + second;
};

std::cout << l1(2, 3) << std::endl;

The rules for passing arguments to a lambda are the same as for a function, and so are the rules for returning results. In fact, with the exception of the rules for capture, most rules for lambdas are borrowed from the rules for functions and classes. However, two irregularities should be noted:

  • [1] If a lambda expression does not take any arguments, the argument list can be omitted.
  • [2] A lambda expression’s return type can be deduced from its body. This is not the case for a function.

Lambda Return Type

The return type of a lambda expression is automatically deduced. You don't have to use the auto keyword unless you specify a trailing-return-type. The trailing-return-type resembles the return-type part of an ordinary function. However, the return type must follow the parameter list, and you must include the trailing-return-type keyword -> before the return type.

If a lambda body does not have a return statement, the lambda’s return type is void. If a lambda body consists of just a single return-statement, the lambda’s return type is the type of the return’s expression. If neither is the case, you have to explicitly supply a return type.

auto r2 = [](int i){ return i; }; // return type is int

Lambda Body

The body of a lambda expression can contain anything that the body of an ordinary method or function can contain.

The body of both a lambda expression and ordinary functions can access these kinds of variables:

  • Captured variables from the enclosing scope

  • Parameters

  • Locally-declared variables

  • Class data members, when declared inside a class and this is captured

  • Any variable that has static storage duration for example, global variables

Using Lambdas within Class Member Functions

You may want to use lambdas inside class member functions. A lambda is a unique class of its own so when it executes it has its own context. So it doesn't automatically have direct access to any of the class’ member variables.

You can include class members in the set of names captured by adding this to the capture list: [this].

To capture a class member variables you must capture the this pointer of the class. You will then have full access to all the class’ data (including private data, as we are inside a member function).

   class Players
{
public:
    int get_players_online()
    {
    std::for_each(player_ids.begin(), player_ids.end(),[this](int i) {if (i > criterion) ++count; });

        return count;
    }

private:
    std::vector<int> player_ids{ 1230, 1689, 8201 };
    int criterion{ 1492 };
    int count{};
};

Members are always captured by reference. That is, [this] implies that members are accessed through this rather than copied into the lambda.

Conclusion

Lambda expressions in C++ are a powerful, easy to use, built-in way for you to simplify and improve your C++ code.

References :

Learn more about the benefits of programming your applications with modern C++

Related services: C++ Software Development