Lambdas & Closures

What’s the Problem?

Remember the example we used, when we were talking about Function Objects in the Standard Template Library topic?

//
//  main.cpp
//  STL Function Objects
//
//  Created by Bryan Higgs on 10/23/24.
//

#include <iostream>
#include <vector>  // For vector
#include <numeric>  // For accumulate()

class Multiply
{
public:
  int operator()(int x, int y) const { return x * y; }
};

int main(int argc, const char * argv[])
{
  int values[] = {2, 3, 5, 7, 11};
  size_t len = sizeof(values)/sizeof(values[0]);
  
  // Initialize vector using pointer iterator
  std::vector<int> v(&values[0], &values[len]);
  
  int product = accumulate(v.begin(), v.end(), 1, Multiply());
  std::cout << "Result: " << product << std::endl;
  
  return 0;
}

which outputs the following:

Result: 2310
Program ended with exit code: 0

where we provided a class that acted as a function object because it defined an operator() method.

Enter Lambdas

Sometimes it’s annoying to have to create an entire class to perform something that is only used once, and locally.

Beware!

You are entering a C++ realm of strange, weird, arcane, and abbreviated syntax.

So, we can do the following:

//
//  main.cpp
//  Lambdas
//
//  Created by Bryan Higgs on 2/15/2025.
//

#include <iostream>
#include <vector>  // For vector
#include <numeric>  // For accumulate()

int main(int argc, const char * argv[])
{
  int values[] = {2, 3, 5, 7, 11};
  size_t len = sizeof(values)/sizeof(values[0]);
  
  // Initialize vector using pointer iterator
  std::vector<int> v(&values[0], &values[len]);
  
  int product = accumulate(v.begin(), v.end(), 1, 
                           [](int x, int y) // Our lambda
                           {
                             return x * y;
                           }
                          );
  std::cout << "Result: " << product << std::endl;
  
  return 0;
}

which also outputs the same result:

Result: 2310
Program ended with exit code: 0

So, what did we do?

What is that really strange syntax? It’s a lambda expression.

Lambda Expressions

Aside from the eleventh letter of the Greek alphabet ( ), what is a Lambda?.

A lambda expression (also called a lambda or closure) allows us to define an anonymous function inside another function.

The nesting is important, as it allows us both to avoid namespace naming pollution, and to define the function as close to where it is used as possible.

The syntax of a Lambda Expression is:

[ capture-clause ] ( parameter-list ) -> return-type { statements; }
  • The capture-clause can be empty if no captures are needed.
  • The parameter-list can be empty if no parameters are required. It can also be omitted entirely unless a return type is specified.
  • The return-type is optional, and if omitted, auto will be assumed (type deduction is used to determine the return type).
  • lambdas have no name – they are anonymous, just like numeric literals, etc.

The most trivial lambda expression is:

[] {}; // a lambda with an omitted return type, no captures, and omitted parameters.

It doesn’t do anything.

auto in C++

In C++, the auto keyword instructs the compiler to deduce the type of a variable from its initialization expression. Introduced in C++11, it simplifies code, especially when dealing with complex types, by eliminating the need to explicitly declare the type.

When auto is used, the compiler infers the variable’s type based on the expression used to initialize it. For example:

auto x = 10; // x is deduced as int
auto y = 3.14; // y is deduced as double
auto z = "hello"; // z is deduced as const char*

auto is particularly useful in situations involving templates, lambda expressions, and complex return types, where explicitly specifying the type can be verbose or cumbersome. It enhances code readability and reduces the risk of type-related errors.

However, auto should be used judiciously. Overuse can sometimes reduce code clarity, especially when the deduced type is not immediately obvious. It’s generally recommended to use auto when the type is clear from the initialization or when dealing with complex types.

Storing a Lambda Expression in a Variable

In the above example, we defined the lambda where it was needed. This use of a lambda is sometimes called a function literal.

But we can store the lambda expression in a variable:

//
//  main.cpp
//  Lambdas
//
//  Created by Bryan Higgs on 2/15/2025.
//

#include <iostream>
#include <vector>  // For vector
#include <numeric>  // For accumulate()

int main(int argc, const char * argv[])
{
  int values[] = {2, 3, 5, 7, 11};
  size_t len = sizeof(values)/sizeof(values[0]);
  auto multiply
  {
    [](int x, int y)
    {
      return x * y;
    }
  };
  
  // Initialize vector using pointer iterator
  std::vector<int> v(&values[0], &values[len]);
  
  int product = accumulate(v.begin(), v.end(), 1, multiply);
  std::cout << "Result: " << product << std::endl;
  
  return 0;
}

which also outputs the same result:

Result: 2310
Program ended with exit code: 0

Much more readable, don’t you think?

But didn’t we invent lambdas to make them local, and nameless?

Passing a Lambda Expression to a Function

There are four ways of passing a lambda expression to a function:

//
//  main.cpp
//  Lambdas
//
//  Created by Bryan Higgs on 2/15/2025.
//

#include <iostream>
#include <functional>

// Case 1: use a `std::function` parameter
void repeat1(int repetitions, const std::function<void(int)>& fn)
{
  for (int i{ 0 }; i < repetitions; ++i)
    fn(i);
}

// Case 2: use a function template with a type template parameter
template <typename T>
void repeat2(int repetitions, const T& fn)
{
  for (int i{ 0 }; i < repetitions; ++i)
    fn(i);
}

// Case 3: use the abbreviated function template syntax (C++20)
void repeat3(int repetitions, const auto& fn)
{
  for (int i{ 0 }; i < repetitions; ++i)
    fn(i);
}

// Case 4: use function pointer (only for lambda with no captures)
void repeat4(int repetitions, void (*fn)(int))
{
  for (int i{ 0 }; i < repetitions; ++i)
    fn(i);
}

int main()
{
  auto lambda = [](int i)
  {
    std::cout << i << std::endl;
  };

  std::cout << "repeat1 :" << std::endl;
  repeat1(3, lambda);
  std::cout << "repeat2 :" << std::endl;
  repeat2(3, lambda);
  std::cout << "repeat3 :" << std::endl;
  repeat3(3, lambda);
  std::cout << "repeat4 :" << std::endl;
  repeat4(3, lambda);

  return 0;
}
  1. Using a std::function parameter

    Here, we can see what the parameters and return type of the std::function are, but adds some overhead because of implicit conversion of the lambda.
  2. Using a function template with a type parameter

    Here, we use a template type parameter, T. It involves a function instantiation where T matches the actual type of the lambda. More efficient than case 1., but the parameters and return type of T are not clear.
  3. Using the abbreviated function template syntax

    Here, the compiler generates a function template identical to case 2.
  4. Using a function pointer (only for lambda with no captures)

    Here, since a lambda with no captures implicitly converts to a function pointer, and so we can pass a lambda

This program outputs:

repeat1 :
0
1
2
repeat2 :
0
1
2
repeat3 :
0
1
2
repeat4 :
0
1
2
Program ended with exit code: 0

Lambda Captures

Let’s look at another example.

Remember how we used the STL find() algorithm with a std::vector?

Here’s a variation of that. It uses find_if():

//
//  main.cpp
//  Lambdas
//
//  Created by Bryan Higgs on 2/15/2025.
//

#include <iostream>
#include <vector>  // For vector
#include <algorithm>  // For find()
#include <string_view>

using namespace std;

int main(int argc, const char * argv[])
{
  vector<string_view> vector{ "owl", "thrush", "woodpecker", "cardinal", "nuthatch"};
  auto found{ find_if( vector.begin(), vector.end(),
                       [](std::string_view str)
                       {
                         return str.find("nut") != std::string_view::npos;
                       }
                     ) };
  
  if ( found == vector.end() )
  {
    cout << "Not found" << endl;
  }
  else
  {
    cout << "Found " << *found << endl;
  }
  
  return 0;
}

which outputs:

Found nuthatch
Program ended with exit code: 0

Explanation

The above code may need some explanation:

std::string_view provides read-only access to an existing string (a C-style string, a std::string, or another std::string_view) without making a copy. It is used to eliminate unnecessary string copies, which tend to be expensive.

find_if() Returns an iterator to the first element in the range that matches the third parameter passed (in this case, our lambda expression). If no such element is found, the function returns last.

found() is an iterator initialized from the call to find_if().
auto is a convenient way for us to ask the compiler to supply the type.

std::string_view::npos is a static member constant of the std::string_view class in C++. It represents the maximum possible value for the size_t type, typically used to indicate the absence of a valid position within the string view. It is often used as a return value by member functions like find, rfind, etc., to signal that the search or operation failed to find a match.

Now, let’s generalize the program, to ask the user what they would like to search for:

//
//  main.cpp
//  Lambdas
//
//  Created by Bryan Higgs on 2/15/2025.
//

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>

using namespace std;

int main(int argc, const char * argv[])
{
  vector<string_view> vector{ "owl", "thrush", "woodpecker", "cardinal", "nuthatch"};

  // Ask the user what to search for.
  std::cout << "What would you like to search for? ";

  std::string search{}; // Storage for user input
  std::cin >> search;   // Obtain user's string

  auto found{ find_if( vector.begin(), vector.end(), 
                        [](std::string_view str) // lambda
                        {
                          // Search for user's specified string.
                          return str.find(search) != std::string_view::npos;
                             // Variable 'search' cannot be implicitly captured
                             // in a lambda with no capture-default specified

                        }
                     )
            };

  if (found == vector.end())
  {
    cout << "Not found" << endl;
  }
  else
  {
    cout << "Found " << *found << endl;
  }

  return 0;
}

But we get a compile-time error:

Variable 'search' cannot be implicitly captured in a lambda with no capture-default specified

What’s the problem?

Unlike nested blocks, where any identifier accessible in the outer block is accessible in the nested block, lambdas can only access certain kinds of objects that have been defined outside the lambda. This includes:

  • Objects with static (or thread local) storage duration (including global variables and static locals)
  • Objects that are constexpr (explicitly or implicitly)

Since search fulfills none of these requirements, the lambda can’t see it.

We can fix this situation by specifying the necessary variable(s) in the lambda’s capture-clause:

//
//  main.cpp
//  Lambdas
//
//  Created by Bryan Higgs on 2/15/2025.
//

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>

using namespace std;

int main(int argc, const char * argv[])
{
  vector<string_view> vector{ "owl", "thrush", "woodpecker", "cardinal", "nuthatch"};

  // Ask the user what to search for.
  std::cout << "What would you like to search for? ";

  std::string search{}; // Storage for user input
  std::cin >> search;   // Obtain user's string

  auto found{ find_if( vector.begin(), vector.end(), 
                        [search](std::string_view str) // lambda
                        {
                          // Search for user's specified string.
                          return str.find(search) != std::string_view::npos;
                        }
                     )
            };

  if (found == vector.end())
  {
    cout << "Not found" << endl;
  }
  else
  {
    cout << "Found " << *found << endl;
  }

  return 0;
}

Here’s what the program outputs when run:

What would you like to search for? pecker
Found woodpecker
Program ended with exit code: 0

(the text in italics is what the user entered)

Lambdas vs Closures

The difference between a lambda expression and a closure is sometimes confusing, since both are often discussed together.

Scott Meyers has described them as follows:

The term “lambda” is short for lambda expression, and a lambda is just that: an expression. As such, it exists only in a program’s source code. A lambda does not exist at runtime.

The runtime effect of a lambda expression is the generation of an object. Such objects are known as closures.

Given

  auto f = [&](int x, int y) { return fudgeFactor * (x + y); };

the expression to the right of the “=” is the lambda expression (i.e., “the lambda”), and the runtime object created by that expression is the closure.

You could be forgiven for thinking that, in this example, f was the closure, but it’s not. f is a copy of the closure. The process of copying the closure into f may be optimized into a move (whether it is depends on the types captured by the lambda), but that doesn’t change the fact that f itself is not the closure. The actual closure object is a temporary that’s typically destroyed at the end of the statement.

The distinction between a lambda and the corresponding closure is precisely equivalent to the distinction between a class and an instance of the class. A class exists only in source code; it doesn’t exist at runtime. What exists at runtime are objects of the class type.  Closures are to lambdas as objects are to classes. This should not be a surprise, because each lambda expression causes a unique class to be generated (during compilation) and also causes an object of that class type–a closure–to be created (at runtime).

– Scott Meyers https://scottmeyers.blogspot.com/2013/05/lambdas-vs-closures.html

For example:

//
//  main.cpp
//  Closures
//
//  Created by Bryan Higgs on 4/4/25.
//

#include <iostream>
#include <functional>

std::function<void(void)> closureWrapper1()
{
  int x = 10;
  return [x]()
  {
    std::cout << "Value in the closure: " << x << std::endl;
  };
}

std::function<void(void)> closureWrapper2()
{
  int x = 10;
  return [&x]()
  {
    x += 1;
    std::cout << "Value in the closure: " << x << std::endl;
  };
}

int main(int argc, const char * argv[])
{
  int x = 10;
  auto func0 = [&x]()
  {
    x += 1;
    std::cout << "Value in the closure: " << x << std::endl;
  };
  
  std::function<void(void)> func1 = closureWrapper1();
  std::function<void(void)> func2 = closureWrapper2();
  
  func0();
  func0();
  func0();
  std::cout << "-------------------------" << std::endl;
  func1();
  func1();
  func1();
  std::cout << "-------------------------" << std::endl;
  func2();
  func2();
  func2();
  
  return 0;
}

which outputs (at least in my compiler run time):

Value in the closure: 11
Value in the closure: 12
Value in the closure: 13
-------------------------
Value in the closure: 10
Value in the closure: 10
Value in the closure: 10
-------------------------
Value in the closure: 2
Value in the closure: 3
Value in the closure: 4
Program ended with exit code: 0

func0 is a closure; it captures the reference to the variable x in the scope of main. Consequently, every time func0 is called, the value of x gets increased by 1.

func1 and func2 are not closures; they are std::function wrapper objects that wrap closures.

func1 captures the value of the variable x in the scope of closureWrapper1 by making a copy of it. Consequently, every time func1 is called, the value of the closure is always 10. After returning from the ordinary function, the local variables in the that function will be out of scope.

func2 captures the reference to the variable x in the scope of closureWrapper2. The reference remembers the address of x. However, after the function returns, the local variable x in the ordinary function will be out of scope. As a result, the value will be undefined.

As Scott Meyers says:

I noted above that a closure is typically destroyed at the end of the statement in which it is created.  The exception to this rule is when you bind the closure to a reference. The simplest way to do that is to employ a universal reference,

  auto&& rrefToClosure = [&](int x, int y) { return fudgeFactor * (x + y); };

but binding it to an lvalue-reference-to-const will also work:

  const auto& lrefToConstToClosure = [&](int x, int y) { return fudgeFactor * (x + y); };

– Scott Meyers https://scottmeyers.blogspot.com/2013/05/lambdas-vs-closures.html

Index