Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 59

Lambda Expressions with STL Algorithms

Recommended resources

Hello! Welcome to the final lesson in our module on Modern C++ and Generic Programming.

In our last lesson, we built on our knowledge of templates to create generic classes, which serve as blueprints for data structures like std::vector or std::array. This showed you how to write highly reusable, type-safe components.

Today, we'll explore another powerful feature of modern C++ that promotes concise and expressive code: lambda expressions. The goal of this lesson is for you to use lambda expressions as anonymous functions, especially with STL algorithms.

You've likely encountered similar concepts in other languages, such as arrow functions in TypeScript/JavaScript or lambda functions in Python. In C++, lambdas are a cornerstone of modern programming style, especially when interacting with the Standard Template Library. They allow you to define small, "on-the-fly" functions right where you need them, making your code cleaner and more readable.

1. The "Why": From Function Objects to Lambdas

Before C++11 introduced lambdas, passing custom logic to an algorithm like std::sort was more verbose. Imagine you have a std::vector of structs and want to sort them based on a specific member. The default std::sort wouldn't know how.

The traditional solution was to create a function object, also known as a functor. This is a class or struct that overloads the function call operator (). This allows an object of the class to be "called" like a function.

To understand this motivation and see how lambdas provide a more elegant solution, the following video offers an excellent explanation.

The C++ Lambdas

This video from Code for yourself introduces the problem of sorting custom objects and explains how function objects (functors) were used to solve it, before showing how lambdas simplify this process.

Please watch the following segments: 00:00 - 01:03: This introduces the problem of sorting a vector of custom structs and how lambdas solve it. 01:39 - 03:54: This explains what a 'callable' is and demonstrates how function objects (functors) can be used to pass stateful comparison logic to std::sort. 05:38 - 06:11: This part makes the key connection: lambdas are essentially 'syntactic sugar' for creating function objects in a more concise, inline way.

As you saw, a lambda expression is, under the hood, a shorthand for the compiler to automatically generate an anonymous function object for you. This is a perfect example of the "under the hood" details that help build a deeper understanding of the language.


2. The Anatomy of a Lambda Expression

A lambda has a distinct syntax that packs a lot of information into a small space. Let's break it down.

C++ Lambda Expression Syntax and Example
This image provides a clear visual breakdown of a lambda's syntax, showing the capture clause, parameter list, and body.

The general structure is:
[capture_clause](parameters) -> return_type { body }

Let's examine each part.

The Capture Clause []

This is the most unique and powerful feature of a lambda. It specifies which variables from the enclosing scope the lambda can access.

  • []: An empty capture clause. The lambda cannot access any variables from the surrounding scope.
  • [=]: Capture by value. All variables used in the lambda from the outer scope are copied. The lambda works with its own copies.
  • [&]: Capture by reference. All variables used in thelambda are accessed by reference. The lambda can modify the original variables.
  • [x, &y]: Explicit capture. x is captured by value, and y is captured by reference. This is the recommended practice for clarity and safety.

Capturing by reference introduces a lifetime dependency. If the lambda is executed after the referenced variable goes out of scope (e.g., in an asynchronous operation), you will have a dangling reference, leading to undefined behavior. This is a critical consideration in systems programming.

C++ Lambda Captures Example
This image shows a lambda with a mix of captures: `a` by reference, `b` by value, and a new variable `c` initialized from a function call (a C++14 feature).

For a handy reference on all the capture possibilities, the following article provides a clear table.

Lambda Functions in C++

This article from DEV Community provides a concise summary of lambda syntax, and its table of capture lists is particularly useful as a reference.

Read the section 'Lambda capture list' and review the table summarizing the different capture options. Also, note the part on 'Capturing member variables' which explains the use of [this].

Parameters () and Return Type -> T

  • Parameters: These work just like in a regular function. You list the type and name of each parameter the lambda accepts. If the lambda takes no parameters, you can omit the parentheses (unless you need to specify mutable or a return type).
  • Return Type: The compiler can usually deduce the return type from the return statement(s) in the body. If there's a single return statement, the return type matches the type of the returned expression. If there are no return statements, the return type is void. You only need to explicitly specify it with -> T in more complex cases, like when there are multiple return statements of different (but convertible) types.

The Body {} and mutable

The body contains the code that the lambda executes.

By default, variables captured by value ([=] or [var]) are const inside the lambda; you cannot modify them. If you need to modify the copy of a captured variable, you must mark the lambda as mutable.

int counter = 0;

// This lambda captures 'counter' by value.
auto my_lambda = [counter]() mutable {
    counter++; // OK: modifies the lambda's internal copy of counter.
    std::cout << "Lambda internal counter: " << counter << std::endl;
};

my_lambda(); // Prints "Lambda internal counter: 1"
my_lambda(); // Prints "Lambda internal counter: 2"

std::cout << "Original counter: " << counter << std::endl; // Prints "Original counter: 0"

The mutable keyword allows the lambda to have an internal state that persists across calls, but it does not affect the original variable that was captured by value.


3. Lambdas in Action with STL Algorithms

The primary motivation for lambdas was to simplify working with STL algorithms. Instead of defining a named function or a full functor class, you can pass your logic directly as an argument.

Let's see this in a practical example using std::find_if, which searches a range for the first element that satisfies a certain condition.

Lambdas in C++

This video from The Cherno demonstrates the syntax and a practical application of lambdas with the STL algorithm std::find_if.

Watch from 08:46 to 11:04. This section shows how to use a lambda as a predicate for std::find_if to search for a value in a std::vector. This is a classic and very common use case.

As you can see, the lambda [](int value) { return value > 3; } is a short, self-contained piece of logic that clearly expresses the condition "the value is greater than 3". Writing this inline makes the code more readable because the logic is right next to the algorithm that uses it.

Your Turn: Putting it all together

Let's apply this to the sorting problem from the beginning of the lesson.

Consider the following setup:




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

struct Track {
    std::string title;
    int duration_seconds;
};

void print_tracks(const std::vector<Track>& tracks) {
    for (const auto& track : tracks) {
        std::cout << "- " << track.title << " (" << track.duration_seconds << "s)\n";
    }
}

int main() {
    std::vector<Track> playlist = {
        {"The Final Countdown", 311},
        {"Bohemian Rhapsody", 355},
        {"Stairway to Heaven", 482},
        {"Hotel California", 391}
    };

    std::cout << "Original playlist:\n";
    print_tracks(playlist);

    // TODO: Sort the playlist by duration, shortest to longest.
    // Use std::sort and a lambda expression here.
    std::sort(playlist.begin(), playlist.end(), /* Your lambda goes here */ );
    
    std::cout << "\nSorted by duration:\n";
    print_tracks(playlist);

    return 0;
}

Try to complete the std::sort call. The third argument should be a lambda that takes two const Track& arguments and returns true if the first track's duration is less than the second's.

Solution
std::sort(playlist.begin(), playlist.end(), 
    [](const Track& a, const Track& b) {
        return a.duration_seconds < b.duration_seconds;
    }
);

Conclusion

You have now learned about lambda expressions, one of modern C++'s most powerful features for writing clean, concise, and effective code.

Key Takeaways:

  • Lambdas are anonymous function objects that the compiler generates for you, providing a concise alternative to traditional functors.
  • The capture clause [] is their key feature, allowing them to access variables from their enclosing scope by value or by reference.
  • Explicit captures ([var, &other_var]) are generally preferred for code clarity and safety.
  • Lambdas integrate seamlessly with STL algorithms like std::sort, std::find_if, and std::for_each, allowing you to define custom logic right where it's needed.

Next Up

This lesson concludes our module on modern C++ features for generic programming and resource management. We've covered RAII, smart pointers, move semantics, templates, and now lambdas.

In the next module, we will shift our focus to another critical aspect of building robust applications: Error Handling and Build Systems. Our first lesson will dive into C++ exceptions, exploring how to use try, catch, and throw to handle runtime errors in a structured way, and we will compare this approach to the C-style error codes you might encounter in lower-level libraries.