Skip to content

Modern C++ • Lambda Expressions

Prior to C++11 the only way to use something resembling a closure in C++ required either third party libraries (such as Boost Bind), rolling your own workarounds (usually with function objects) or using proprietary compiler-specific extensions. I remember the first time i stumbled into a C++ closure in the good old days when Borland (R.I.P) added the __closure extension to their C++ compiler in order to implement the event-driven architecture of their RAD framework (the legendary Borland C++ Builder, the first real RAD C++ IDE in history). The implementation was limited though as it could only capture the context of a single object and not that of generic code blocks (the extension basically packed the this pointer with a function pointer into a 64-bit variable).

For those coming to C++ from scripting languages such as Javascript, Python, PHP and the like, where closures are a native construct, the lack of this useful and powerful feature must have been really disheartening. Closures provide a clean mechanism to capture state and attach it to a block of code “on the fly” for deferred processing, very useful for building event-driven applications for example, so native support that eliminates the implementation of clumsy workarounds is beneficial to the developers community.

Before delving into crude boring code let’s recall some useful concepts that will allow us to better understand the underlying mechanism. There is often confusion between the terms closure and lambda expression (or lambda function) which are often used interchangeably. In reality they refer to two different things, even though related of course. A lambda expression is the lexical definition of an anonymous function, that is a function that is not identified by a name, while a closure is the instantiation by the compiler of a lambda expression bound to its enclosing scope. Generally speaking, a closure is a programming technique to bind a block of code to the variables in the scope where the block is defined (a function with state). The set of variables bound to the lambda function form the environment of the closure when the lambda function is instantiated. The following picture depicts the whole concept

A C++ lambda expression has the following syntax

[<captures>*](<params>)* <specs>* <exc>* <ret-type>* {<body>}

where the clauses are defined as follows (* denotes optional clauses)

<captures>        ::=  <capture-default> | 
                       <capture-list> | 
                       <capture-default>, 
                       <capture-list>
<capture-default> ::=  & | =
<capture-list>    ::=  <capture> | <capture-list>, <capture>
<capture>         ::=  variable | &variable | this
<params>          ::=  param | <params> param
<specs>           ::=  mutable | constexpr
<exc>             ::=  throw() | noexcept
<ret-type>        ::=  -> return-type
<body>            ::=  lambda-code

In the above expression, [] specifies the capture environment, that is the variables captured from the enclosing scope, which can be a default “catch-all” [&] indicating that all variables are captured by reference, or [=] indicating that all variables are captured by value, or it can be a list of specific captured variables by value or reference [var1, &var2, var3, ..., this], including the this pointer, or a default catch-all and a list of specific variables, or it can be void [] indicating that no variables are captured. A list of parameters passed to the lambda function may optionally follow, along with the optional specifiers mutable|constexpr, an optional exception specification throw()|noexcpt, an optional return type, and finally the body of the lambda function.

Let’s have a look at some example code

auto closure = []{ std::cout << "I'm a lambda function!" << std::endl; };

closure(); // prints "I'm a lambda function!"

the above code defines a lambda expression/function that does not capture anything, has no parameters and does not return a value. This lambda expression generates a closure object, which is an rvalue that has an unspecified unique type generated at the point of occurrence and automatically inferred with the auto keyword. The closure can then be used like a normal function using the function operator to invoke the lambda code. Technically, this closure object is internally implemented using a function object along with other mechanisms to construct the environment, but all this verbose implementation is hidden behind a more concise native construct that’s way easier to use. This means that C++ closures are “callable” and can be wrapped into generic function objects, like in the following code

// Create a function object that "wraps" the lambda
std::function<void()> f = []{ std::cout << "I'm a lambda function!" << std::endl; };

f(); // prints "I'm a lambda function!"

This is, however, an indirection as the type of a lambda is only known to the compiler. Let’s see some more examples

int x = 1, y = 2, z = 3;

// A lambda with a default capture by reference returning an int
auto f = [&]() -> int { return x + y + z; };
	
std::cout << f() << std::endl; // prints 6

x = 2;

std::cout << f() << std::endl; // prints 7

In the above code we define a lambda function that captures all the variables in the enclosing scope by reference and returns an integer (the return type can be omitted if the compiler can infer it). As can be seen, if any of the captured variables is modified the changes will be reflected in the closure’s environment. Variables captured by reference introduce dependencies tied to the captured variables’ lifetime so one must be cautious when using them from within a lambda function as they may have been modified outside or even no longer exist.

int x = 1, y = 2, z = 3;

// A lambda with a default capture by value returning an int
auto f = [=]() -> int { return x + y + z; };
	
std::cout << f() << std::endl; // prints 6

x = 2;

std::cout << f() << std::endl; // prints 6

The code above is the same as the previous one but this time the scope’s variables are captured by value. We can also provide a comma-separated list of specific variables to be captured (in any order) by reference or value, like in the following example

int x = 1, y = 2, z = 3;

// A lambda capturing x by value and y by reference
auto f = [x, &y]() -> int { return x + y; };
	
std::cout << f() << std::endl; // prints 3

y = 5;

std::cout << f() << std::endl; // prints 6

Default captures can be used together with capture lists in the form [capture-default, capture-list] to specify that some variables must not be captured with the default method. Note that none of the variables in the capture-list must match the default (they must all be captured with another method).

int x = 1, y = 2, z = 3;

// Default capture by reference, excluded x and z
auto f = [&,x,z]() -> int { return x + y + z; };

// Default capture by value, excluded z
auto f2 = [=, &z]() -> int { return x + y + z; };

// ERROR: x matches the default capture
auto f3 = [=, x, &y]() -> int { return x + y + z; };

Lambda functions can take a list of parameters, as any other named function. C++11 did not allow default parameters in lambda expressions, but since C++14 it’s a supported feature.

int x = 1, y = 2, z = 3;

// Default capture by value with parameters
auto f = [=](int p, int d) -> int { return std::pow(x + y + z, p) + d; };
// Since C++14 default parameters are supported
auto f2 = [=](int p, int d = 0) -> int { return std::pow(x + y + z, p) + d; };

std::cout << f(3, 5) << std::endl; // prints 221
std::cout << f(3) << std::endl; // prints 216

It is also possible to compose lambda functions by using them as parameters and return values. This technique implements what is called High-Order-Lambdas. Since the type of a lambda function is unspecified we have to use them as function objects in order to be passed around. The below code illustrates this and the comments explain in detail what happens under the hood

int main()
{
    int x = 10;

     // This lambda captures the variables of the enclosing scope (main(){}),
     // that is 'x', which become part of its environment => Env(h) = {x}
     auto h = [=](int a) -> std::function<int()> {

          // This nested lambda captures the variables of its enclosing lambda,
          // that is 'a', and the outer scope (main(){}) => Env(nested) = {a} U {x}
          auto nested = [=]() -> int {
                return x + a + 1;  
          };
          return nested;
     };

     // Calls the lambda function h() and get the returned lambda function
     // as a function object. Since the parameter 'a' is in g()'s environment
     // the value 3 will be used by g()
     auto g = h(3);

     // Prints the result (14)
     std::cout << g() << std::endl;

     // This lambda captures nothing and accepts a lambda function in one of
     // its parameters (as a function object), returning an int
     auto f = [](std::function<int()>& g, int v) -> int {
          return g() - v;
     };

     // Calls f() by passing g() and the result of executing g(), which is the
     // same as before since its environment hasn't been modified
     std::cout << f(g, g()) << std::endl; // prints 0

     return 0;
}

Consider now the following code

int x = 1;

// ERROR
auto f = [x]() -> int { x++; return x; };

If you try to compile this code it will complain about x not being modifiable. This is because the function call operator () used to call the lambda is by default declared as const. To allow modification of variables captured by value within the lambda the mutable keyword must be added to the lambda expression, as follows

int x = 1;

// OK
auto f = [x]() mutable -> int { x++; return x; };

Lambda functions can be called immediately without first assigning them to a variable by using a trailing function call operator, like in the following code

// This lambda is called immediately when the code is executed ...
[] { std::cout << "I'm called immediately!" << std::endl; }();

// ... and this one too
[](int a, int b) { std::cout << "The result is " << a + b << std::endl; }(1,2);

When used within classes, lambda functions can access the class data members by capturing the this pointer. The this pointer can be captured either implicitly or explicitly. When captured implicitly it is always captured by reference. When captured explicitly, it can be captured by reference with [this] or by value with [*this] (since C++17), in which case a copy of the object will be created (useful if the lambda will be called asynchronously at a later stage when the original object may no longer exist). The following example demonstrates these concepts

class MyClass {

	void print()
	{
		std::cout << "You just called me from a lambda!" << std::endl;
	}

public:

	void call_me()
	{
		// Captures the this pointer explicitly by reference
		[this] { print(); }();

		// Captures the this pointer explicitly by value (since C++17)
		[*this] { print(); }();

		// Captures the this pointer implicitly by reference
		[=] { print(); }();

		// Same as above
		[&] { print(); }();
	}
};

int main()
{
	MyClass hey;
	hey.call_me();

	return 0;
}

// Output:
// "You just called me from a lambda!"
// "You just called me from a lambda!"
// "You just called me from a lambda!"
// "You just called me from a lambda!"

One of the most common use cases for closures is as an argument for another function that will use it in its execution. Many algorithms in the Standard Library accept closures as a parameter that will be used when the algorithm is executed. While this could be achieved by other means as well (e.g. with function objects) doing it with closures is certainly more clean and intuitive. A few examples are shown in the below code

// Old C++ (using only language constructs and a pure OOP approach)

class print_f {
   public: void operator()(const std::string& name) {
	   std::cout << name << " is " << name.length() << " characters long.\n";
   }
};

class start_with_C_f {
   public: bool operator()(const std::string& name) {
	   return name.at(0) == 'C';
   }
};

int main(int argc, char** argv)
{
	std::vector<std::string> names{
		"Albert", "Christian", "Jocelyn"
	};

	std::for_each(names.begin(), names.end(), print_f());

	auto name = std::find_if(names.begin(), names.end(), start_with_C_f());

	std::cout << "Found " << (*name) << std::endl;

	return 0;
}

// Modern C++

int main(int argc, char** argv)
{
	std::vector<std::string> names {
		"Albert", "Christian", "Jocelyn"
	};

	std::for_each(names.begin(), names.end(), [](const std::string& name)
	{
		std::cout << name << " is " << name.length() << " characters long.\n";
	});

	auto name = std::find_if(names.begin(), names.end(), [](const std::string& name)
	{
		return name.at(0) == 'C';
	});

	std::cout << "Found " << (*name) << std::endl;

	return 0;
}

But the power of closures goes far beyond these simple examples and a more thorough presentation would require a separate article. I have the feeling that this feature is one of the most underestimated by C++ programmers, who appear to be dismissing it as “stuff for web developers” (who are probably the ones who use it the most). The versatility of closures goes well beyond web development and the few examples illustrated in this article. Closures provide an easy mechanism to capture and transmit scoped state bound to a block of code in an immediate, clean and intuitive manner without cluttering the code with verbose workarounds (or external libraries) which is very useful to develop asynchronous systems and in Functional Programming.

Published inModern C++