본문으로 건너뛰기 [2026] C++ Lambda Capture — Value vs Reference, init capture, `this`, and pitfalls

[2026] C++ Lambda Capture — Value vs Reference, init capture, `this`, and pitfalls

[2026] C++ Lambda Capture — Value vs Reference, init capture, `this`, and pitfalls

이 글의 핵심

Lambda capture defines how a lambda accesses variables from its enclosing scope. You can capture by copy or by reference; the choice affects safety, performance, and whether updates are visible outside the lambda.

What is lambda capture?

Lambda capture defines how a lambda accesses variables from its enclosing scope. A lambda can capture names from the scope where it is defined; depending on the capture form, it holds a copy or a reference to each entity.

Example:

int x = 10;

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

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

// Default: capture everything by value
auto f3 = [=]() { return x; };

// Default: capture everything by reference
auto f4 = [&]() { return x; };

Why capture exists

  • Closures: the lambda can “remember” outer state.
  • Flexibility: choose copy vs reference per variable.
  • Conciseness: less boilerplate than a hand-written functor.
  • Type safety: the compiler checks what you capture.
// Functor: more 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: shorter
int x = 10;
auto add10 = [x](int y) { return x + y; };
std::cout << add10(5) << '\n';  // 15

How it is implemented

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

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);

Value capture vs reference capture

int x = 10;

// Value capture: a copy inside the closure
auto f1 = [x]() mutable {
    x++;  // modifies the copy
    return x;
};

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

// Reference capture: refers to the original
auto f2 = [&x]() {
    x++;  // modifies the original
    return x;
};

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

Mixed capture

int x = 10;
int y = 20;

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

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

Init capture (C++14)

// Introduce a new member with an initializer
auto f1 = [x = 42]() {
    return x;
};

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

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

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

Practical examples

Example 1: counter

makeCounter returns a lambda that holds its own counter state.

auto makeCounter() {
    int count = 0;

    return [count]() mutable {
        return ++count;
    };
}

int main() {
    auto counter = makeCounter();

    std::cout << counter() << std::endl;  // 1
    std::cout << counter() << std::endl;  // 2
    std::cout << counter() << std::endl;  // 3
}

Example 2: filter

std::vector<int> filterGreaterThan(const std::vector<int>& vec, int threshold) {
    std::vector<int> result;

    std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
        [threshold](int x) {
            return x > threshold;
        });

    return result;
}

int main() {
    std::vector<int> nums = {1, 5, 3, 8, 2, 9, 4};
    auto filtered = filterGreaterThan(nums, 5);

    for (int n : filtered) {
        std::cout << n << " ";
    }
    std::cout << std::endl;  // 8 9
}

Example 3: event handler

class Button {
private:
    std::function<void()> onClick;

public:
    void setOnClick(std::function<void()> handler) {
        onClick = handler;
    }

    void click() {
        if (onClick) {
            onClick();
        }
    }
};

int main() {
    Button button;
    int clickCount = 0;

    // Reference capture for clickCount
    button.setOnClick([&clickCount]() {
        clickCount++;
        std::cout << "Click " << clickCount << '\n';
    });

    button.click();
    button.click();
    button.click();
}

Example 4: sorting

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };

    std::sort(people.begin(), people.end(),
        [](const Person& a, const Person& b) {
            return a.age < b.age;
        });

    for (const auto& p : people) {
        std::cout << p.name << ": " << p.age << std::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 *this (C++17)
        return [*this]() mutable {
            return ++count;  // modifies the copy’s view of members
        };
    }

    int getCount() const {
        return count;
    }
};

int main() {
    Counter counter;
    auto inc = counter.getIncrementer();

    std::cout << inc() << std::endl;           // 1
    std::cout << inc() << std::endl;           // 2
    std::cout << counter.getCount() << std::endl;  // 2
}

The mutable keyword

int x = 10;

// Copy capture: members are const in operator() by default
auto f1 = [x]() {
    // x++;  // error: const
    return x;
};

// mutable: can modify the captured copy
auto f2 = [x]() mutable {
    x++;  // OK (copy)
    return x;
};

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

Common problems

Problem 1: dangling reference

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

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

// Good: value capture
std::function<int()> makeFuncOk() {
    int x = 10;
    return [x]() { return x; };
}

Problem 2: missing capture

int x = 10;
int y = 20;

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

// Good: capture both
auto f = [x, y]() {
    return x + y;
};

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

Problem 3: this lifetime

class Widget {
public:
    auto getCallback() {
        // Dangerous if Widget is destroyed before the lambda runs
        return [this]() {
            // UB if *this is gone
        };
    }

    // Safer: shared ownership
    auto getCallback(std::shared_ptr<Widget> self) {
        return [self]() {
            (void)self;
        };
    }
};

Capture cheat sheet

[]        // no capture
[x]       // x by value
[&x]      // x by reference
[=]       // default: all automatic variables by value
[&]       // default: all automatic variables by reference
[=, &x]   // x by reference, others by value
[&, x]    // x by value, others by reference
[this]    // capture this pointer
[*this]   // copy the object (C++17)
[x = 42]  // init capture (C++14)

Production-oriented patterns

Pattern 1: deferred execution

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 chain

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: simple 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: Value capture vs reference capture?

A

  • [x] (by value): safer (own copy), copy cost, does not mutate the original.
  • [&x] (by reference): no copy, risk of dangling references, can mutate the original.
int x = 10;

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

Rule of thumb

  • If the lambda escapes the function (returned, stored): prefer value (or clear ownership such as shared_ptr).
  • If the lambda is used only locally while everything is alive: reference can be fine.

Q2: When do I need mutable?

A When you need to modify copy-captured members inside operator(). Copies are const by default unless the lambda is mutable.

int x = 10;

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

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

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

Q3: [=] vs [&]?

A

  • [=]: capture all automatic variables by value (safer, may copy a lot).
  • [&]: capture all by reference (fast, easy to create dangling references).
int x = 10, y = 20;

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

In real code, explicit capture such as [x, &y] is often clearer and safer.

Q4: When do I use this capture?

A Inside member functions when the lambda must use members of *this. [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 A C++14 feature: introduce a new closure member with = or move from an existing object.

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

  • Value capture: copying large objects can be expensive.
  • Reference capture: no copy, but lifetime must outlive all uses.
  • Move init capture: transfer ownership without a deep copy (C++14).
std::vector<int> vec(1000000);

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

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

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

Q7: Further reading?

A

Related posts: Lambda complete, Init capture.

One-line summary: Lambda capture lets a closure hold outer names by value or reference so the body can use them safely—or unsafely if lifetimes are wrong.


See also