Skip to Content
Course content

85: Lambda Captures in Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're sending a field agent out on a mission. The agent is the lambda function. Now, the agent can't possibly bring the entire headquarters with them—that would be inefficient and impossible. Instead, they carry a "mission kit" (the capture clause). Some items in that kit are just photos of documents back at HQ (captured by value); if the agent scribbles on the photo, the original document at HQ stays clean. Other items are high-frequency radios (captured by reference); if the agent transmits a change through the radio, the people back at HQ see that change in real-time.

In C++, the capture clause [] is exactly that kit. It tells the compiler which variables from the surrounding scope need to be "packed" into the lambda's internal structure so they're available when the lambda is actually executed, potentially long after the original scope has vanished.

Snapshots versus Tethers

When you use [=], you're telling the lambda to take a snapshot of everything it needs. It's safe, but it can be slow if you're copying a massive std::vector. When you use [&], you're creating a tether. This is fast, but it's where most of the "weird" bugs live.


void runFilter(const std::vector<int>& scores, int threshold) {
    // Capture 'threshold' by value because it's a small int.
    // We don't need to change the original threshold.
    auto isHighScorer = [threshold](int s) { 
        return s > threshold; 
    };

    // If we wanted to count how many passed, we'd need a tether to a counter.
    int count = 0;
    auto incrementCount = [&count]() { 
        count++; 
    };

    for(int s : scores) {
        if(isHighScorer(s)) incrementCount();
    }
    std::cout << "High scorers: " << count << "\n";
}

I usually advise against using the "capture all" shortcuts [=] or [&] in production code. It's too easy to accidentally capture a heavy object by value or a temporary variable by reference. Be explicit. If you need threshold and count, write [threshold, &count]. It makes your intentions clear to the next developer (which might be you in six months).

The Dangling Reference Trap

Here is the mistake I see most often: returning a lambda that captures a local variable by reference. If the lambda outlives the function it was created in, that reference points to a memory location that has been reclaimed. It's a classic "use-after-free" scenario.

If you're returning a lambda from a function to be used as a callback, always capture by value. If you need the lambda to modify something, that "something" needs to live in a std::shared_ptr or be passed in as an argument during the call, not captured from the stack.

Bridging the Gap to Class Members

Lambdas inside class methods are a bit special. You can't actually capture individual member variables (like this->m_score) directly. You have to capture the this pointer.

In C++11/14, [this] gives you a reference to the current object. But if the object is deleted before the lambda runs, you're back to the dangling reference problem. C++17 introduced [*this], which creates a full copy of the object. It's a lifesaver for asynchronous code where the object might be destroyed while a background task is still processing.


class GameSession {
    int sessionID = 101;
    
    auto getLogger() {
        // Captures the object by value (C++17)
        // Even if GameSession is destroyed, the lambda has its own copy of sessionID.
        return [*this]() {
            std::cout << "Log from session: " << sessionID << "\n";
        };
    }
};

Moving Heavy Objects into Captures

Sometimes you have a std::unique_ptr or a massive buffer that you can't copy, but you can't leave it in the local scope either. This is where "init-captures" (C++14) come in. You can actually move a variable into the lambda's scope.


auto createProcessor() {
    auto data = std::make_unique<std::vector<int>>(1000000);
    
    // We move 'data' into the lambda. 
    // 'data' is now null in the outer scope, and owned by the lambda.
    return [buffer = std::move(data)]() {
        std::cout << "Processing " << buffer->size() << " elements\n";
    };
}



📋 Practical Task

Implementing a Dynamic Inventory Filter

You are building a system for an RPG inventory. You need to create a function that returns a "Filter Lambda." This lambda should be able to check if an item meets two criteria: a specific category (string) and a minimum value (int). Both the category and the minimum value are determined at the moment the filter is created, not when it is used.

Requirements:

  • Define a simple Item struct with std::string name, std::string category, and int value.
  • Write a function createItemFilter(std::string cat, int minVal) that returns a lambda.
  • The lambda must capture cat and minVal by value to ensure the filter remains valid even after createItemFilter returns.
  • The lambda should take a const Item& as an argument and return true if the item matches the category AND its value is greater than or equal to the minimum value.
  • In your main, create a std::vector<Item>, generate a filter for "Weapon" with a value of 50, and use std::count_if or a loop to find how many items match.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.