C++ 람다 캡처 | "Lambda Capture" 완벽 가이드
이 글의 핵심
람다 캡처(Lambda Capture) 는 람다 함수가 외부 변수에 접근하는 방법을 정의합니다. 람다는 자신이 정의된 스코프의 변수를 캡처하여 사용할 수 있으며, 캡처 방식에 따라 값 복사 또는 참조로 접근합니다.
람다 캡처란?
람다 캡처(Lambda Capture) 는 람다 함수가 외부 변수에 접근하는 방법을 정의합니다. 람다는 자신이 정의된 스코프의 변수를 캡처하여 사용할 수 있으며, 캡처 방식에 따라 값 복사 또는 참조로 접근합니다.
int x = 10;
// 값 캡처
auto f1 = [x]() { return x; };
// 참조 캡처
auto f2 = [&x]() { return x; };
// 모든 변수 값 캡처
auto f3 = [=]() { return x; };
// 모든 변수 참조 캡처
auto f4 = [&]() { return x; };
왜 필요한가?:
- 클로저: 람다가 외부 상태를 “기억”할 수 있음
- 유연성: 값 또는 참조로 선택적 캡처
- 간결성: 함수 객체 대신 간단한 문법
- 타입 안전: 컴파일러가 캡처 검증
// ❌ 함수 객체: 복잡
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
// ✅ 람다 캡처: 간결
int x = 10;
auto add10 = [x](int y) { return x + y; };
std::cout << add10(5) << '\n'; // 15
캡처의 동작 원리:
람다는 내부적으로 익명 함수 객체(Functor) 로 변환됩니다. 캡처된 변수는 함수 객체의 멤버 변수가 됩니다.
int x = 10;
auto f = [x]() { return x; };
// 개념적으로 다음과 같이 변환됨:
struct __lambda {
int x; // 캡처된 변수
__lambda(int x) : x(x) {}
int operator()() const { return x; }
};
__lambda f(x);
값 캡처 vs 참조 캡처
int x = 10;
// 값 캡처: 복사본
auto f1 = [x]() mutable {
x++; // 복사본 수정
return x;
};
cout << f1() << endl; // 11
cout << x << endl; // 10 (원본 변경 안됨)
// 참조 캡처: 원본
auto f2 = [&x]() {
x++; // 원본 수정
return x;
};
cout << f2() << endl; // 11
cout << x << endl; // 11 (원본 변경됨)
혼합 캡처
int x = 10;
int y = 20;
// x는 값, y는 참조
auto f = [x, &y]() {
// x++; // 에러: 값 캡처는 const
y++; // OK: 참조 캡처
return x + y;
};
cout << f() << endl; // 31
cout << x << endl; // 10
cout << y << endl; // 21
초기화 캡처 (C++14)
// 새 변수 생성
auto f1 = [x = 42]() {
return x;
};
// 이동 캡처
auto ptr = make_unique<int>(10);
auto f2 = [p = move(ptr)]() {
return *p;
};
// 표현식 캡처
int x = 10;
auto f3 = [y = x * 2]() {
return y;
};
cout << f3() << endl; // 20
실전 예시
예시 1: 카운터
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
}
예시 2: 필터
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
}
예시 3: 이벤트 핸들러
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;
// clickCount 참조 캡처
button.setOnClick([&clickCount]() {
clickCount++;
cout << "클릭 " << clickCount << "회" << endl;
});
button.click(); // 클릭 1회
button.click(); // 클릭 2회
button.click(); // 클릭 3회
}
예시 4: 정렬
struct Person {
string name;
int age;
};
int main() {
vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// age로 정렬
sort(people.begin(), people.end(),
{
return a.age < b.age;
});
for (const auto& p : people) {
cout << p.name << ": " << p.age << endl;
}
// Bob: 25
// Alice: 30
// Charlie: 35
}
this 캡처
class Counter {
private:
int count = 0;
public:
auto getIncrementer() {
// this 캡처 (멤버 접근)
return [this]() {
return ++count;
};
}
auto getIncrementerCopy() {
// this 복사 (C++17)
return [*this]() mutable {
return ++count; // 복사본 수정
};
}
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
}
mutable 키워드
int x = 10;
// 값 캡처는 기본적으로 const
auto f1 = [x]() {
// x++; // 에러: const
return x;
};
// mutable로 수정 가능
auto f2 = [x]() mutable {
x++; // OK (복사본 수정)
return x;
};
cout << f2() << endl; // 11
cout << x << endl; // 10 (원본 유지)
자주 발생하는 문제
문제 1: 댕글링 참조
// ❌ 댕글링 참조
function<int()> makeFunc() {
int x = 10;
return [&x]() { return x; }; // x는 소멸됨
}
auto f = makeFunc();
// cout << f() << endl; // UB: x는 이미 소멸
// ✅ 값 캡처
function<int()> makeFunc() {
int x = 10;
return [x]() { return x; }; // 복사본
}
문제 2: 캡처 누락
int x = 10;
int y = 20;
// ❌ y 캡처 누락
auto f = [x]() {
return x + y; // 에러: y 캡처 안됨
};
// ✅ y 캡처
auto f = [x, y]() {
return x + y;
};
// 또는 모든 변수 캡처
auto f = [=]() {
return x + y;
};
문제 3: this 수명
class Widget {
public:
auto getCallback() {
// ❌ this 댕글링
return [this]() {
// Widget이 소멸되면 UB
};
}
// ✅ shared_ptr 사용
auto getCallback(shared_ptr<Widget> self) {
return [self]() {
// 안전
};
}
};
캡처 방식 정리
[] // 캡처 없음
[x] // x를 값으로 캡처
[&x] // x를 참조로 캡처
[=] // 모든 변수 값 캡처
[&] // 모든 변수 참조 캡처
[=, &x] // x는 참조, 나머지는 값
[&, x] // x는 값, 나머지는 참조
[this] // this 포인터 캡처
[*this] // this 객체 복사 (C++17)
[x = 42] // 초기화 캡처 (C++14)
실무 패턴
패턴 1: 지연 실행
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 << "작업 1: " << x << '\n';
});
scheduler.schedule([&x]() {
x++;
std::cout << "작업 2: " << x << '\n';
});
scheduler.executeAll();
패턴 2: 콜백 체인
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 << "작업 완료: " << result << '\n';
});
패턴 3: 상태 머신
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: 값 캡처 vs 참조 캡처?
A:
- 값 캡처
[x]: 안전 (복사본 사용), 복사 비용 발생, 원본 변경 불가 - 참조 캡처
[&x]: 빠름 (복사 없음), 댕글링 위험, 원본 변경 가능
int x = 10;
// 값 캡처: 안전하지만 복사 비용
auto f1 = [x]() { return x; };
// 참조 캡처: 빠르지만 수명 주의
auto f2 = [&x]() { return x; };
선택 기준:
- 람다가 함수 밖으로 반환되면: 값 캡처
- 람다가 로컬에서만 사용되면: 참조 캡처
Q2: mutable은 언제 사용하나요?
A: 값 캡처한 변수를 수정할 때 사용합니다. 값 캡처는 기본적으로 const이므로 mutable이 필요합니다.
int x = 10;
// ❌ 에러: 값 캡처는 const
auto f1 = [x]() {
// x++; // 에러
};
// ✅ mutable: 복사본 수정 가능
auto f2 = [x]() mutable {
x++; // OK (복사본 수정)
return x;
};
std::cout << f2() << '\n'; // 11
std::cout << x << '\n'; // 10 (원본 유지)
Q3: [=] vs [&]?
A:
[=]: 모든 변수를 값으로 캡처 (안전, 복사 비용)[&]: 모든 변수를 참조로 캡처 (빠름, 댕글링 위험)
int x = 10, y = 20;
// [=]: 모든 변수 값 캡처
auto f1 = [=]() { return x + y; };
// [&]: 모든 변수 참조 캡처
auto f2 = [&]() { return x + y; };
실무 권장: 명시적 캡처 [x, &y]가 더 명확하고 안전합니다.
Q4: this 캡처는 언제 사용하나요?
A: 멤버 함수에서 멤버 변수나 멤버 함수에 접근할 때 사용합니다.
class Counter {
int count_ = 0;
public:
auto getIncrementer() {
// [this]: this 포인터 캡처
return [this]() {
return ++count_;
};
}
auto getIncrementerCopy() {
// [*this]: 객체 복사 (C++17)
return [*this]() mutable {
return ++count_; // 복사본 수정
};
}
};
Q5: 초기화 캡처는 무엇인가요?
A: C++14에서 도입된 기능으로, 캡처 시 새 변수를 생성하거나 이동 캡처를 수행합니다.
// 새 변수 생성
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: 람다 캡처 시 성능 고려사항은?
A:
- 값 캡처: 복사 비용 (큰 객체는 참조 권장)
- 참조 캡처: 복사 없음 (빠름)
- 이동 캡처: 복사 없이 소유권 이전 (C++14)
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: 람다 캡처 학습 리소스는?
A:
- “Effective Modern C++” (Item 31-34) by Scott Meyers
- cppreference.com - Lambda expressions
- “C++ Lambda Story” by Bartłomiej Filipek
관련 글: Lambda Complete, Init Capture.
한 줄 요약: 람다 캡처는 외부 변수를 값 또는 참조로 캡처하여 람다 내부에서 사용할 수 있게 합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 람다 함수 | “익명 함수” 완벽 정리 [캡처/mutable]
- C++ Init Capture | “초기화 캡처” 가이드
- C++ Generic Lambda | “제네릭 람다” 가이드
관련 글
- C++ 람다 캡처 에러 |
- C++ 람다 함수 |
- C++ std::function vs 함수 포인터 |
- C++ constexpr Lambda |
- C++ Generic Lambda |