Lambdas in C++ - A Complete Tutorial

Lambdas in C++ - A Complete Tutorial

In Modern C++, Lambdas were introduced with C++11 to make the life of Developers easier. But what exactly is a Lambda Function, and why do we need it? In this tutorial we will learn about Lambda Functions in C++ in detail with examples.

What is a Lambda in C++? #

Lambda functions are anonymous functions, which means they don’t have a name. However, they can accept arguments like normal functions and can also return values like normal functions, despite lacking a name.

Now, the question is: We invoke a normal function by its name, but how can we invoke a Lambda function that doesn’t have name associated with it?

We can define a Lambda Function and store it in a variable. Then, at some point later, we can invoke the Lambda Function through that variable. Essentially, Lambda Functions are a convenient way to define short code snippets that can be passed around, like small one-liners or callbacks.

What we need Lambdas in C++? #

Before looking more into lambda expressions; like how to create and use them, we need to understand why Lambda Functions are necessary. Let’s explore this with an example. Imagine we have a vector of strings, and we want to sort them in descending order based on their length.

std::vector<std::string> cities = {"London", "Pune", "Las Vegas", "Yokohama"};

In other words, the longest string should come first, and the shortest one last. To do this, we can use the std::sort() function, passing the vector’s begin and end iterators. As a third argument, we need to pass a comparator that compares the strings based on their size. Typically, we would need to create a separate function for this, like so:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>

bool comparator(
        const std::string& first,
        const std::string& second)
{
    return first.size() > second.size();
}
int main()
{

    std::vector<std::string> cities = {"London", "Pune", "Las Vegas", "Yokohama"};
    
    // Sort a vector of strings by the length of strings in descending order
    std::sort(cities.begin(), cities.end(), comparator);

    // Print the contents of vector on console
    std::copy(cities.begin(), cities.end(), std::ostream_iterator<std::string>(std::cout, ", "));

    return 0;
}

It will sort the vector of strings in sorted order like this,

Las Vegas, Yokohama, London, Pune, 

Here, we created a global function that accepts two strings and compares them based on their size. We then pass this function as the third argument to std::sort(), which sorts the strings in the vector. However, this function is only needed for sorting and won’t be used anywhere else, making it unnecessary “code clutter.”

This is where Lambda Functions come in. Instead of defining a separate function, we can define a Lambda Function inline while calling std::sort():

// Sort a vector of strings by the 
// length of strings in descending order
std::sort(
    cities.begin(),
    cities.end(), 
    [](const std::string& first, const std::string& second) {
        return first.size() > first.size();
    });

Now, the Lambda Function is used as the comparator directly inside the call to std::sort(). There’s no need to define a separate function. Lambda functions make it easier to write inline functions, especially in situations where we don’t need a named function.

Defining a Lambda in C++ #

The basic syntax of a Lambda Function looks like this:

[ captures ] ( parameters ) -> return_type { body }

Let’s break this down:

  1. Captures: The capture block (inside square brackets) specifies external variables that should be accessible inside the lambda. Variables can be captured by value or by reference. If you don’t need to capture any external variables, you can leave this empty.
  2. Parameters: These are the input parameters, similar to regular functions. Whenever the Lambda Function is invoked, these parameters are passed.
  3. Return Type: This is optional because, in most cases, the compiler can deduce the return type based on the return expression.
  4. Body: This is the actual function body, which can contain multiple statements, loops, conditionals, and more.

Let’s see an example where we define a Lambda Function that compares two strings by their length:

auto compare =  [](const std::string& first, const std::string& second) {
                        return first.size() > first.size();
                };

In this Lambda Function:

  • We have an empty capture block because no external variables are needed, as of now.
  • The parameters are two constant string references.
  • The return type is automatically deduced, so there’s no need to specify it.

We have assigned the lambda function to a variable compare, and now we can invoke this Lambda Function like a normal function, using this variable. Like this,

bool result = compare("London", "Las Vegas");

This line will invoke the lambda function and it will compare two given strings by thier length and returns 1 if the first string is longer than second string, otherwise returns 0.

std::function & Lambdas #

In the previous example, we store the lambda function in a auto variable. Instead of using auto, we can store a lambda in an std::function. For example, we can create a variable that can store a lambda function that returns an int and accepts two constant string references,

// A Function Variable to store lambda function
std::function<int (const std::string&, const std::string&)> funcObj;

Then we can assign a lambda function to this variable like this,

// Assign a lambda function to a variable 
funcObj  =  [](const std::string& first, const std::string& second) {
                return first.size() > first.size();
            };

Now our variable funcObj contains a lambda function. To invoke this lambda using variable funcObj we can use it as a function and the pass parameters to it, like this,

int result = funcObj ("London", "Las Vegas");

It will invoke the lambda function, which will internally compare two strings and returns 0, as the first string is smaller in size as compared to second string.

The whole example is as follows,

#include <iostream>
#include <string>
#include <functional> // Required for std::function<>

int main()
{
    // A Function Variable to store lambda function
    std::function<int (const std::string&, const std::string&)> funcObj;

    // Assign a lambda function to a variable 
    funcObj  =  [](const std::string& first, const std::string& second) {
                    return first.size() > first.size();
                };
    
    std::cout << funcObj ("London", "Las Vegas") << std::endl;
    return 0;
}

It will print,

0

The benefit of using std::function is that it allows us to pass lambdas (or other callable objects) as parameters to other functions. For example, let’s create a function that accepts a lambda:

// A function that accept a callback as argument
void sampleFunction(
        std::function<int(const std::string&, const std::string&)> func)
{
    // Calling the callback function
    std::cout << func ("London", "Las Vegas") << std::endl;
}

We can call sampleFunction like this:

// Pass Lambda function as parameter to another function
sampleFunction( [](const std::string& first, const std::string& second) {
                    return first.size() > first.size();
                });

Basically, we passed a callback/lambda function to the sampleFunction(), and it can now invoke this lambda function whenever needed. Prior to Modern C++, when lambdas were not introduced, we used to pass function pointers as arguments to other functions for callbacks. However, for that, we needed to define a separate function and then pass it as a function pointer. But with lambdas, we can define an anonymous function and pass it as a parameter to the other function, without the need to define separate functions.

Examples of Lambdas in C++ #

Sort a Vector of strings #

Now let’s return to our original problem of sorting a vector of strings by length. Using a Lambda Function with std::sort(), we can achieve this in a single line. Checkout the complete example,

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>

int main()
{
    std::vector<std::string> cities = 
                {"London", "Pune", "Las Vegas", "Yokohama"};
    
     // Sort a vector of strings by the 
     // length of strings in descending order
    std::sort(
        cities.begin(),
        cities.end(), 
        [](const std::string& first, const std::string& second) {
            return first.size() > first.size();
        });

    // Print the contents of vector on console
    std::copy(
        cities.begin(),
        cities.end(),
        std::ostream_iterator<std::string>(std::cout, ", "));

    return 0;
}

We created a vector of strings and then called the std::sort() function, passing the iterators pointing to the beginning and end of the vector. As the third parameter, we passed a lambda function that accepts two strings as arguments and compares them based on their length. This will sort the vector of strings in decreasing order based on their length. The contents of the sorted vector will be as follows:

London, Pune, Las Vegas, Yokohama

Using for_each() wih Lambdas #

Lambdas are also useful with other STL algorithms like std::for_each. Let’s say we have a vector of integers and we want to print only those greater than 5:

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

int main()
{

    std::vector<int> numbers = {1, 2, 6, 7, 8, 3, 4, 10};

    std::for_each(
        numbers.begin(), // Iterator to the start of vector
        numbers.end(),   // Iterator to the end of vector
        [](int num) {    // Lambda to print integer greater than 5
            if (num > 5) {
                std::cout << num << ", ";
            }
        });

    return 0;
}

This will output the numbers greater than 5 i.e.

6, 7, 8, 10,

Again, the Lambda Function is defined and used inline, making the code more concise and readable.

Here is the revised version of your text with improved grammar and clarity:

Till now, we have covered the basics of lambda functions, we learned how to write and use them, but we haven’t yet explored how to capture external variables inside a lambda function. Let’s understand that now.

What are Lambda Captures? #

We can also capture External Variables inside Lambda Functions. To understand that let’s go over the syntax of lambda functions again.

[ captures ] ( parameters ) -> return_type { body }

In the lambda syntax, we have the capture block. In the examples we’ve seen so far, we haven’t passed anything in the capture block, but we can pass multiple values in the capture block to capture external variables. Let’s understand this with an example:

Suppose we have a vector of integers, and we want to add a value x to each integer and print them one by one using the STL algorithm std::for_each() and a lambda function. Let’s first try it without capturing any external variables in the lambda function.


int x = 5;
std::vector<int> vec = {1, 3, 5, 6};

// This code will not work
// Error: x is not known inside lambda
std::for_each(vec.begin(), vec.end(), [](int num) {
    std::cout << num + x << ", ";
});

This code will give an error because we are trying to access the variable x inside the lambda function. But how can the lambda function know what x is if we have not captured any external variable?

To access any external variable inside a lambda function, we need to capture it in the capture block while defining the lambda function. In this case, we can capture the external variable x while defining the lambda function, and then we can use it inside the lambda function. Let’s see the code:

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

int main()
{

    int x = 5;
    std::vector<int> vec = {1, 3, 5, 6};

    std::for_each(vec.begin(), vec.end(), [=](int num) {
        std::cout << num + x << ", ";
    });

    return 0;
}

Output:

6, 8, 10, 11,

In the capture block of the lambda function, inside the square brackets, we passed = i.e.

// Capture external variables by value
[=](int num) {
    std::cout << num + x << ", ";
}

It copied all the external variables into the lambda function. Remember, it will copy all the external variables that are in scope at the time of defining the lambda function. For example, it copied the variable x inside the lambda function. Inside the lambda body, we added x to the parameter received and printed it.

So, in the above code, the for_each() function will iterate over all the elements of the vector, and for each element, it will call the lambda function, which will add x to the parameter received and print it.

This is how we can capture external variables inside a lambda function. There are three different ways to capture external variables in a lambda function:

  • Capture all external variables by value inside the lambda function.
  • Capture all external variables by reference inside the lambda function.
  • Capture some external variables by reference and some by value inside the lambda function.

Let’s understand this with examples.

Capturing by Value #

To make the external variable available inside the lambda function, we can capture it by value. This is done by adding the variable to the capture block. In this case, it copies the value of x into the lambda function:

std::for_each(vec.begin(), vec.end(), [x](int num) {
    std::cout << num + x << std::endl;
});

When we run this program, it will print the sum of each element and x. Capturing by value means the external variable is copied into the lambda function, and any changes made to it inside the lambda won’t affect the original variable outside the lambda.

You can also capture multiple external variables by value. For example, if we have two variables x and y:

int x = 5;
int y = 3;
std::vector<int> vec = {1, 3, 5, 6};

std::for_each(vec.begin(), vec.end(), [x, y](int num) {
    std::cout << num + x + y << ", ";
});

This will add both x and y to each element of the vector. Output will be,

9, 11, 13, 14,

To capture all the variables by Value use can pass = in the capture block, like this,

std::for_each(vec.begin(), vec.end(), [=](int num) {
    std::cout << num + x + y << ", ";
});

Capturing by Reference #

We can also capture external variables by reference, which allows us to modify the external variable inside the lambda function. To capture by reference, use the & symbol in the capture block:

int x = 5;
int y = 3;
std::vector<int> vec = {1, 3, 5, 6};

std::for_each(vec.begin(), vec.end(), [&x, &y](int num) {
    x++;
    y++;
});
std::cout << "x = " << x << std::endl;
std::cout << "y = " << y << std::endl;

Output will be,

x = 9
y = 7

Here, both x and y are captured by reference. Each time the lambda is invoked, x and y are incremented by 1. After the std::for_each loop, the values of x and y will be incremented by the number of elements in the vector. Since we capture by reference, the changes made inside the lambda will reflect outside as well.

To capture all the variables by reference use can pass & in the capture block, like this,

std::for_each(vec.begin(), vec.end(), [&](int num) {
    x++;
    y++;
});

Capturing Specific Variables #

In previous examples, we captured all external variables either by value or by reference. But what if we want to capture specific variables by value and others by reference? We can do that by specifying each variable explicitly:

int x = 5;
int y = 10;
int z = 15;
std::vector<int> vec = {1, 3, 5, 6};

std::for_each(vec.begin(), vec.end(), [x, &y](int num) {
    // x is captured by value, y is captured by reference
    std::cout << num + x + y << ", ";
    y++;
});
std::cout << std::endl;
std::cout << "x = " << x << std::endl;
std::cout << "y = " << y << std::endl;

Output will be,

16, 19, 22, 24, 
x = 5
y = 14

In this case, x is captured by value, so modifications to x inside the lambda won’t affect the original x. On the other hand, y is captured by reference, so any changes to y inside the lambda will affect the original y.

If we try to modify x inside the lambda, it will result in a compiler error because it is captured by value and treated as const. To modify x, we can make the lambda mutable as shown earlier.

Mutable Lambdas in C++ #

If you capture by external variables inside lambda function by value, the captured variables are treated as const by default. This means you cannot modify them inside the lambda function. If you try to do so, you will get a compiler error:

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

int main()
{

    int x = 5;
    std::vector<int> vec = {1, 3, 5, 6};

    std::for_each(vec.begin(), vec.end(), [x](int num) {
        std::cout << num + x << ", ";
        // Modify external variable captured by copy
        x++;
    });

    return 0;
}

Output:

example8.cpp: In lambda function:
example8.cpp:14:9: error: increment of read-only variable ‘x’
   14 |         x++;
      |         ^

It happens because we have captured variable x by copy inside the lambda function and by default it creads a readonly copy, i.e. const. Now to modify these type of external variables we need to make lambda function mutuable i.e.

int x = 5;
std::vector<int> vec = {1, 3, 5, 6};

std::for_each(vec.begin(), vec.end(), [x](int num) mutable {
    std::cout << num + x << ", ";
    // Modify external variable captured by copy
    x++;
});

std::cout << std::endl;
std::cout << "x = " << x << std::endl;

Output:

6, 9, 12, 14, 
x = 5

By adding the mutable keyword to the lambda, we can modify the captured variables, even though they are captured by value. However, the modifications are only local to the lambda function, and the original variable outside the lambda remains unchanged.

Summary #

That’s all for this tutorial. In this, we learned what a lambda function is and how to use it. Then we learned different ways to capture external variables inside the lambda function. In the next article, we will explore how to capture the this pointer to call member functions inside a lambda. We will also see how to explicitly specify the return type of a lambda function and discuss scenarios where this is necessary. Finally, we’ll explore nested lambdas and how to use them."

Author: Varun

A Software Developer with 20 Years of Experience in C/C++.