Lambda Expressions

Lambda expressions (or lambdas) were recently added to C++, giving us a more concise alternative to functions. Let’s start by taking a look at how they’re used:

#include <iostream>
#include <string>

int main() {
    std::string name = "Matthew";
    auto helloLambda = [&name] (int number) -> void {
        std::cout << "Hello " << name << "!\n";
        std::cout << "Number: " << number << "\n";
    };
    helloLambda(5);
    return 0;
}

Program Output:

Hello Matthew!
Number: 5

Here, we create a lambda that prints some simple data, and we store it into the helloLambda variable. Then, just like a function, the lambda can be called by using parentheses containing its arguments.

At first, lambdas can look pretty awkward since they involve lots of different parts. Let’s try to clear some things up…

auto

auto is a special data type in C++ that is automatically inferred. For example, in “auto x = 5;“, x will be inferred to be an int, since 5 is an integer.

The data type of a lambda is unspecified, meaning that there’s no way for us to determine what a lambda’s type is. For this reason, we must always use auto as a lambda’s data type, letting the computer figure things out for us.

Structure of a Lambda

Here’s the basic structure of a lambda, numbered for reference:

auto lambda = [1] (2) -> 3 {
    4
};
1. Capture Clause

The capture clause is a comma-separated list of variables that exist outside of the lambda and will be used inside of the lambda. For instance, in the example program, name had to be put in the capture clause so that it could be used within the lambda. The reason for putting an ampersand before name is because outside variables can be captured in two different ways:

  • By reference: A reference to the outside variable is taken, so no copy is made. Any modifications to the variable within the lambda are applied to the outside variable.
    • Individual variables are captured by reference by placing an ampersand before the variable name. For example: [&name]
    • To capture all outside variables by reference, the capture clause would look like this: [&]
  • By value: A copy of the outside variable is made for the lambda to use, but this copy is const by default. Therefore, the copy cannot be modified. (To make the copy not const, “mutable” must be placed after the parameter list [number 2].)
    • Individual variables are captured by value by just using the variable name (without an ampersand). For example: [name]
    • To capture all outside variables by value, the capture clause would look like this: [=]

Also, to capture all outside variables except the name variable by reference, the capture clause would look like this: [&, name]

Similarly, to capture all outside variables except the name variable by value, the capture clause would look like this: [=, &name]

An empty capture clause ([]) can be used if the lambda accesses no outside variables.

2. Parameter List

A lambda’s parameter list works just like that of a function. When the lambda is called, a list of arguments, corresponding to each required parameter, is placed in parentheses.

In the example program, the parameter list just consisted of one parameter: int number. Therefore, when calling the lambda with “helloLambda(5);“, the number variable is assigned the number 5.

3. Trailing Return Type

The return type of the lambda is placed here. In the example program, no value is returned, so the return type is void.

Including the return type is optional, as it can automatically be deduced by the compiler.

4. Body

A lambda’s body behaves just like that of a function. Any code that should be executed when calling the lambda goes here.

Applications

At first, lambdas just look like overly-complex functions. When interacting with some of C++’s built-in functions, however, they can be really useful.

Just know that lambdas don’t really allow us to do anything new, but they offer a shorter way of doing things.

Threads

Rather than creating functions for different threads to use in our code, we can put a lambda directly in a thread’s constructor to tell the thread what to do:

#include <iostream>
#include <thread>

int main() {
    std::thread thread1([] () {
        std::cout << "Beginning background task 1...\n";
        for (int i = 0; i < 5; i++) {
            std::cout << "Background task 1: i = " << i << "\n";
        }
        std::cout << "Background task 1 completed!\n";
    });
    
    std::thread thread2([] () {
        std::cout << "Beginning background task 2...\n";
        for (int i = 0; i < 5; i++) {
            std::cout << "Background task 2: i = " << i << "\n";
        }
        std::cout << "Background task 2 completed!\n";
    });

    thread1.join();
    thread2.join();

    return 0;
}

Sample Program Output (may vary):

Beginning background task 1...
Background task 1: i = Beginning background task 2...
Background task 2: i = 0
Background task 2: i = 1
Background task 2: i = 2
Background task 2: i = 3
Background task 2: i = 4
Background task 2 completed!
0
Background task 1: i = 1
Background task 1: i = 2
Background task 1: i = 3
Background task 1: i = 4
Background task 1 completed!
Algorithms

Lots of nice algorithms can be accessed by including <algorithm> in our code, and some of these can use lambdas as arguments. Like with threads, lambdas just make our code shorter, allowing us to avoid creating unnecessary functions.

I’ll go through a few useful functions inside <algorithm>, but you can find a complete list here.

Iterators

Before we begin, however, it’ll be nice to go over how “iterators” work.

Most of the functions in <algorithm> are for interacting with containers (arrays, vectors, etc.). These functions often have a “first” and “last” parameter for specifying the range of elements to work with in a container. Here, first and last are iterators.

Take a look at this program to see how iterators of elements in a container can be found:

#include <vector>

int main() {
    //For arrays, we get iterators using pointers
    int numbersArray[] = {6, 2, 9, 4, 1, 8};

    //Iterator to first element (just a pointer to the first element)
    int *iterator1 = numbersArray; 
    //Iterator to second element (just a pointer to the second element)
    int *iterator2 = numbersArray + 1;
    //End iterator (pointer to 1 past the last element)
    int *iterator3 = numbersArray + 6;


    //For vectors (and many other containers), we get iterators
    //using the begin() and end() functions.
    std::vector<int> numbersVec = {6, 2, 9, 4, 1, 8};

    //Iterator to first element
    std::vector<int>::iterator iterator4 = numbersVec.begin();
    //Iterator to second element
    std::vector<int>::iterator iterator5 = numbersVec.begin() + 1;
    //End iterator (refers to 1 past the last element)
    std::vector<int>::iterator iterator6 = numbersVec.end();
    //Also an end iterator
    std::vector<int>::iterator iterator7 = numbersVec.begin() + 6;
}

Usually the “first” iterator is inclusive and the “last” iterator is exclusive. This next program shows what that would mean:

#include <vector>

int main() {
    int numbersArray[] = {6, 2, 9, 4, 1, 8};
    
    /*
        Working with the whole array would mean
            The "first" iterator is a pointer to the first element
            The "last" iterator is a pointer to one past the last element
    */
    int *first1 = numbersArray; 
    int *last1 = numbersArray + 6;

    /*
        Working with the first, second, and third elements would mean
            The "first" iterator is a pointer to the first element
            The "last" iterator is a pointer to fourth element
    */
    int *first2 = numbersArray; 
    int *last2 = numbersArray + 3;



    std::vector<int> numbersVec = {6, 2, 9, 4, 1, 8};

    /*
        Working with the whole vector would mean
            The "first" iterator is the "begin" iterator (first element)
            The "last" iterator is the "end" iterator (one past the last element)
    */
    std::vector<int>::iterator first3 = numbersVec.begin();
    std::vector<int>::iterator last3 = numbersVec.end();

    /*
        Working with the first, second, and third elements would mean
            The "first" iterator is the "begin" iterator
            The "last" iterator is the "begin" iterator + 3 (fourth element)
    */
    std::vector<int>::iterator first4 = numbersVec.begin();
    std::vector<int>::iterator last4 = numbersVec.begin() + 3;
}
std::count_if

This function counts the number of elements in a container that satisfy a certain condition.

Parameters:

  1. The “first” iterator (inclusive)
  2. The “last” iterator (exclusive)
  3. A function pointer or lambda that
    1. Has one parameter representing a single element selected from the container
    2. Returns true if the parameter satisfies the condition to check for or false if it does not

Here’s std::count_if using a function:

#include <iostream>
#include <algorithm>

bool isEven(int num) {
    return num % 2 == 0;
}

int main() {
    int numbers[] = {6, 2, 9, 4, 1, 8};

    //Count number of evens in the whole array
    int numEvens = std::count_if(numbers, numbers + 6, isEven);
    
    std::cout << numEvens << "\n";
    return 0;
}

And using a lambda:

#include <iostream>
#include <algorithm>

int main() {
    int numbers[] = {6, 2, 9, 4, 1, 8};
    int numEvens = std::count_if(numbers, numbers + 6, [] (int num) {return num % 2 == 0;});
    std::cout << numEvens << "\n";
    return 0;
}

Program Output:

4
std::for_each

This function applies some function on every element in a container.

Parameters:

  1. The “first” iterator (inclusive)
  2. The “last” iterator (exclusive)
  3. A function pointer or lambda that
    1. Has one parameter representing a single element selected from the container
    2. Has a void return type

Here’s std::for_each using a function:

#include <iostream>
#include <algorithm>

void add5(int &num) {
    num += 5;
}

int main() {
    int numbers[] = {6, 2, 9, 4, 1, 8};

    //Add 5 to each element in the array
    std::for_each(numbers, numbers + 6, add5);
    
    std::cout << "New array:\n";
    for (int i = 0; i < 6; i++) {
        std::cout << numbers[i] << "  ";
    }
    std::cout << "\n";
    
    return 0;
}

And using a lambda:

#include <iostream>
#include <algorithm>

int main() {
    int numbers[] = {6, 2, 9, 4, 1, 8};
    std::for_each(numbers, numbers + 6, [] (int &num) {num += 5;});
    
    std::cout << "New array:\n";
    for (int i = 0; i < 6; i++) {
        std::cout << numbers[i] << "  ";
    }
    std::cout << "\n";

    return 0;
}

Program Output:

New array:
11  7  14  9  6  13
std::sort

This function sorts a container based on some comparison function

Parameters:

  1. The “first” iterator (inclusive)
  2. The “last” iterator (exclusive)
  3. A function pointer or lambda that
    1. Has two parameters representing two different elements selected from the container
    2. Returns true if its first parameter should go before the second parameter in the final sorted array and false if not

Here’s std::sort using a function:

#include <iostream>
#include <algorithm>

bool compare(int num1, int num2) {
    return num1 < num2; //Ascending order
}

int main() {
    int numbers[] = {6, 2, 9, 4, 1, 8};
    std::sort(numbers, numbers + 6, compare);
    
    std::cout << "New array:\n";
    for (int i = 0; i < 6; i++) {
        std::cout << numbers[i] << "  ";
    }
    std::cout << "\n";

    return 0;
}

And using a lambda:

#include <iostream>
#include <algorithm>

int main() {
    int numbers[] = {6, 2, 9, 4, 1, 8};
    std::sort(numbers, numbers + 6, [] (int num1, int num2) {return num1 < num2;});
    
    std::cout << "New array:\n";
    for (int i = 0; i < 6; i++) {
        std::cout << numbers[i] << "  ";
    }
    std::cout << "\n";

    return 0;
}

Program Output:

New array:
1  2  4  6  8  9