본문으로 건너뛰기 C++ Lambda Capture — Value vs Reference, init capture,

C++ Lambda Capture — Value vs Reference, init capture,

C++ Lambda Capture — Value vs Reference, init capture,

이 글의 핵심

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


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, lambda, capture, closure, reference.