[2026] C++ Lambda Capture — Value, Reference, and Init Capture

[2026] C++ Lambda Capture — Value, Reference, and Init Capture

이 글의 핵심

Lambda capture defines how a C++ lambda reaches outer variables. Lambdas can capture names from their enclosing scope by copy or by reference, depending on the capture list.

What is lambda capture?

Lambda capture defines how a lambda accesses variables from its surrounding scope. A lambda can capture names from the scope where it is defined and use them inside the body, either by copy or by reference, depending on the capture list.

Example C++:

int x = 10;

// Capture by value
auto f1 = [x]() { return x; };

// Capture by reference
auto f2 = [&x]() { return x; };

// Capture all locals by value
auto f3 = [=]() { return x; };

// Capture all locals by reference
auto f4 = [&]() { return x; };

Why capture matters:

  • Closures: the lambda can remember outer state.
  • Flexibility: choose value vs reference per variable.
  • Concision: less boilerplate than a hand-written functor.
  • Type safety: the compiler checks what you capture.
// Functor: verbose
struct Adder {
    int x;
    Adder(int x) : x(x) {}
    int operator()(int y) const { return x + y; }
};

Adder add10(10);
std::cout << add10(5) << '\n';  // 15

// Lambda capture: short
int x = 10;
auto add10 = [x](int y) { return x + y; };
std::cout << add10(5) << '\n';  // 15

How capture works:

A lambda is lowered to an anonymous function object (functor). Captured variables become data members of that object.

int x = 10;
auto f = [x]() { return x; };

// Conceptually similar to:
struct __lambda {
    int x;
    __lambda(int x) : x(x) {}
    int operator()() const { return x; }
};

__lambda f(x);

Capture by value vs by reference

Example C++:

int x = 10;

// By value: a copy lives in the closure
auto f1 = [x]() mutable {
    x++;  // modifies the copy
    return x;
};

cout << f1() << endl;  // 11
cout << x << endl;     // 10 (original unchanged)

// By reference: aliases the original
auto f2 = [&x]() {
    x++;  // modifies the original
    return x;
};

cout << f2() << endl;  // 11
cout << x << endl;     // 11 (original changed)

Mixed capture

Example C++:

int x = 10;
int y = 20;

// x by value, y by reference
auto f = [x, &y]() {
    // x++;  // error: by-value capture is const unless mutable
    y++;     // OK: reference capture
    return x + y;
};

cout << f() << endl;  // 31
cout << x << endl;    // 10
cout << y << endl;    // 21

Init capture (C++14)

Example C++:

// Introduce a new capture name
auto f1 = [x = 42]() {
    return x;
};

// Move capture
auto ptr = make_unique<int>(10);
auto f2 = [p = move(ptr)]() {
    return *p;
};

// Expression in the capture
int x = 10;
auto f3 = [y = x * 2]() {
    return y;
};

cout << f3() << endl;  // 20

Practical examples

Example 1: Counter

Implementation sketch for makeCounter:

auto makeCounter() {
    int count = 0;
    
    return [count]() mutable {
        return ++count;
    };
}

int main() {
    auto counter = makeCounter();
    
    cout << counter() << endl;  // 1
    cout << counter() << endl;  // 2
    cout << counter() << endl;  // 3
}

Example 2: Filter

vector<int> filterGreaterThan(const vector<int>& vec, int threshold) {
    vector<int> result;
    
    copy_if(vec.begin(), vec.end(), back_inserter(result),
        [threshold](int x) {
            return x > threshold;
        });
    
    return result;
}

int main() {
    vector<int> nums = {1, 5, 3, 8, 2, 9, 4};
    auto filtered = filterGreaterThan(nums, 5);
    
    for (int n : filtered) {
        cout << n << " ";
    }
    cout << endl;  // 8 9
}

Example 3: Event handler

class Button {
private:
    function<void()> onClick;
    
public:
    void setOnClick(function<void()> handler) {
        onClick = handler;
    }
    
    void click() {
        if (onClick) {
            onClick();
        }
    }
};

int main() {
    Button button;
    int clickCount = 0;
    
    // Capture clickCount by reference
    button.setOnClick([&clickCount]() {
        clickCount++;
        cout << "Clicked " << clickCount << " time(s)" << endl;
    });
    
    button.click();
    button.click();
    button.click();
}

Example 4: Sorting

struct Person {
    string name;
    int age;
};

int main() {
    vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    
    // Sort by age
    sort(people.begin(), people.end(),
         [](const Person& a, const Person& b) {
            return a.age < b.age;
        });
    
    for (const auto& p : people) {
        cout << p.name << ": " << p.age << endl;
    }
    // Bob: 25
    // Alice: 30
    // Charlie: 35
}

Capturing this

class Counter {
private:
    int count = 0;
    
public:
    auto getIncrementer() {
        // Capture this (member access)
        return [this]() {
            return ++count;
        };
    }
    
    auto getIncrementerCopy() {
        // Copy of *this (C++17)
        return [*this]() mutable {
            return ++count;  // modifies the copy’s members
        };
    }
    
    int getCount() const {
        return count;
    }
};

int main() {
    Counter counter;
    auto inc = counter.getIncrementer();
    
    cout << inc() << endl;  // 1
    cout << inc() << endl;  // 2
    cout << counter.getCount() << endl;  // 2
}

The mutable keyword

Example C++:

int x = 10;

// By-value capture is const by default
auto f1 = [x]() {
    // x++;  // error: const
    return x;
};

// mutable allows mutating the copy inside the lambda
auto f2 = [x]() mutable {
    x++;  // OK (copy)
    return x;
};

cout << f2() << endl;  // 11
cout << x << endl;     // 10 (original unchanged)

Common problems

Problem 1: Dangling references

Example C++:

// Dangling reference
function<int()> makeFunc() {
    int x = 10;
    return [&x]() { return x; };  // x is destroyed when makeFunc returns
}

auto f = makeFunc();
// cout << f() << endl;  // UB: x is gone

// By-value capture
function<int()> makeFunc() {
    int x = 10;
    return [x]() { return x; };  // copy survives
}

Problem 2: Missing capture

Example C++:

int x = 10;
int y = 20;

// y not captured
auto f = [x]() {
    return x + y;  // error: y not captured
};

// Capture y
auto f = [x, y]() {
    return x + y;
};

// Or default capture
auto f = [=]() {
    return x + y;
};

Problem 3: Lifetime of this

class Widget {
public:
    auto getCallback() {
        // Dangerous if Widget is destroyed before the lambda runs
        return [this]() {
            // UB if Widget is gone
        };
    }
    
    // shared_ptr for extended lifetime
    auto getCallback(shared_ptr<Widget> self) {
        return [self]() {
            // safer if you control lifetime via shared_ptr
        };
    }
};

Capture cheat sheet

Example C++:

[]        // nothing captured
[x]       // x by value
[&x]      // x by reference
[=]       // all used locals by value
[&]       // all used locals by reference
[=, &x]   // default by value, x by reference
[&, x]    // default by reference, x by value
[this]    // capture this pointer
[*this]   // capture a copy of *this (C++17)
[x = 42]  // init capture (C++14)

Production patterns

Pattern 1: Deferred work

class TaskScheduler {
    std::vector<std::function<void()>> tasks_;
    
public:
    void schedule(std::function<void()> task) {
        tasks_.push_back(task);
    }
    
    void executeAll() {
        for (auto& task : tasks_) {
            task();
        }
        tasks_.clear();
    }
};

TaskScheduler scheduler;
int x = 10;

scheduler.schedule([x]() {
    std::cout << "Task 1: " << x << '\n';
});

scheduler.schedule([&x]() {
    x++;
    std::cout << "Task 2: " << x << '\n';
});

scheduler.executeAll();

Pattern 2: Callback chains

class AsyncOperation {
public:
    template<typename F>
    void then(F&& callback) {
        std::thread([callback = std::forward<F>(callback)]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            callback();
        }).detach();
    }
};

AsyncOperation op;
int result = 0;

op.then([&result]() {
    result = 42;
    std::cout << "Done: " << result << '\n';
});

Pattern 3: State machine

class StateMachine {
    std::function<void()> currentState_;
    
public:
    void setState(std::function<void()> state) {
        currentState_ = state;
    }
    
    void execute() {
        if (currentState_) {
            currentState_();
        }
    }
};

StateMachine sm;
int count = 0;

auto idle = [&]() {
    std::cout << "Idle\n";
    if (count++ > 3) {
        sm.setState([&]() {
            std::cout << "Active\n";
        });
    }
};

sm.setState(idle);
sm.execute();

FAQ

Q1: By value vs by reference?

A:

  • [x] by value: safer (uses a copy), costs a copy, does not change the original (unless you only mutate the copy with mutable).
  • [&x] by reference: no copy, can mutate the original, dangling if the referent is destroyed.

Example C++:

int x = 10;

auto f1 = [x]() { return x; };

auto f2 = [&x]() { return x; };

Rule of thumb:

  • If the lambda escapes the function (returned, stored, async): prefer by value.
  • If it is used only locally with obvious lifetime: by reference is often fine.

Q2: When do I need mutable?

A: When you need to modify a by-value capture inside the lambda. Those members behave like const in a const operator() unless you add mutable.

Example C++:

int x = 10;

auto f1 = [x]() {
    // x++;  // error
};

auto f2 = [x]() mutable {
    x++;  // OK (copy)
    return x;
};

std::cout << f2() << '\n';  // 11
std::cout << x << '\n';     // 10

Q3: [=] vs [&]?

A:

  • [=]: capture everything you use by value (safer, copy cost).
  • [&]: capture everything you use by reference (fast, lifetime risk).

Example C++:

int x = 10, y = 20;

auto f1 = [=]() { return x + y; };

auto f2 = [&]() { return x + y; };

Practice: explicit lists like [x, &y] are usually clearer and easier to review.

Q4: When do I capture this?

A: In member functions when the lambda must use members of the current object. [this] stores the pointer; [*this] (C++17) stores a copy of the object.

class Counter {
    int count_ = 0;
    
public:
    auto getIncrementer() {
        return [this]() {
            return ++count_;
        };
    }
    
    auto getIncrementerCopy() {
        return [*this]() mutable {
            return ++count_;
        };
    }
};

Q5: What is init capture?

A: C++14 lets you name captures with arbitrary expressions, including move into the closure.

Example C++:

auto f1 = [x = 42]() { return x; };

auto ptr = std::make_unique<int>(10);
auto f2 = [p = std::move(ptr)]() {
    return *p;
};

int x = 10;
auto f3 = [y = x * 2]() { return y; };

Q6: Performance considerations?

A:

  • By value: copy cost (large objects: consider const reference capture with care, or move capture).
  • By reference: no copy (watch lifetimes).
  • Move capture: transfer ownership without a deep copy (C++14).

Example C++:

std::vector<int> vec(1000000);

auto f1 = [vec]() { return vec.size(); };

auto f2 = [&vec]() { return vec.size(); };

auto f3 = [vec = std::move(vec)]() { return vec.size(); };

Q7: Further reading?

A:

See also: Lambda complete guide, Init capture.

In one line: lambda capture binds outer names into the closure by value or reference so the lambda body can use them safely—if you respect object lifetimes.


More on this topic: