C++ 람다 캡처 에러 | "dangling reference" 크래시와 캡처 실수 해결
이 글의 핵심
C++ 람다 캡처 에러에 대한 실전 가이드입니다.
들어가며: “람다를 저장했더니 크래시가 나요"
"참조 캡처한 변수가 이미 소멸되었어요”
C++11의 람다(Lambda)는 익명 함수를 간결하게 작성할 수 있게 해주지만, 캡처(Capture—람다가 외부 변수를 사용하는 방식)를 잘못 사용하면 댕글링 참조나 예상치 못한 동작이 발생합니다.
// ❌ 댕글링 참조
std::function<int()> createLambda() {
int x = 42;
return [&x]() { return x; }; // x를 참조 캡처
} // x 소멸
int main() {
auto lambda = createLambda();
std::cout << lambda() << '\n'; // ❌ 소멸된 변수 접근 → 크래시
}
이 글에서 다루는 것:
- 값 캡처 vs 참조 캡처
- 댕글링 참조 방지
- this 캡처
- 초기화 캡처 (C++14)
- 자주 나오는 람다 에러 10가지
목차
1. 람다 캡처 기본
캡처 방식
int x = 10;
int y = 20;
// [=]: 모든 변수를 값으로 캡처
auto lambda1 = [=]() { return x + y; };
// [&]: 모든 변수를 참조로 캡처
auto lambda2 = [&]() { x = 30; return x + y; };
// [x]: x만 값으로 캡처
auto lambda3 = [x]() { return x * 2; };
// [&x]: x만 참조로 캡처
auto lambda4 = [&x]() { x = 30; };
// [x, &y]: x는 값, y는 참조
auto lambda5 = [x, &y]() { return x + y; };
// [=, &y]: 기본 값, y만 참조
auto lambda6 = [=, &y]() { y = 30; return x + y; };
mutable 람다
int x = 10;
// ❌ 값 캡처는 수정 불가
auto lambda1 = [x]() {
// x = 20; // 컴파일 에러: cannot assign to a variable captured by copy
};
// ✅ mutable로 수정 가능
auto lambda2 = [x]() mutable {
x = 20; // OK (람다 내부 복사본 수정)
return x;
};
std::cout << lambda2() << '\n'; // 20
std::cout << x << '\n'; // 10 (원본은 변경 안 됨)
2. 값 캡처 vs 참조 캡처
값 캡처 [=]
int x = 10;
auto lambda = [=]() { // x를 복사
return x * 2;
};
x = 20; // 원본 변경
std::cout << lambda() << '\n'; // 20 (캡처 시점의 값: 10)
특징:
- 캡처 시점의 값을 복사
- 원본 변경 영향 없음
- 안전 (댕글링 참조 없음)
참조 캡처 [&]
int x = 10;
auto lambda = [&]() { // x를 참조
return x * 2;
};
x = 20; // 원본 변경
std::cout << lambda() << '\n'; // 40 (현재 값: 20)
특징:
- 원본을 참조
- 원본 변경 즉시 반영
- 위험 (원본 소멸 시 댕글링)
3. 댕글링 참조 방지
문제 코드
// ❌ 댕글링 참조
std::function<int()> createLambda() {
int x = 42;
return [&x]() { return x; }; // x를 참조 캡처
} // x 소멸
int main() {
auto lambda = createLambda();
std::cout << lambda() << '\n'; // ❌ 소멸된 변수 접근
}
해결법 1: 값 캡처
// ✅ 값 캡처
std::function<int()> createLambda() {
int x = 42;
return [x]() { return x; }; // x를 복사
} // x 소멸해도 람다는 복사본 보유
int main() {
auto lambda = createLambda();
std::cout << lambda() << '\n'; // 42 (안전)
}
해결법 2: shared_ptr
// ✅ shared_ptr로 수명 연장
std::function<int()> createLambda() {
auto x = std::make_shared<int>(42);
return [x]() { return *x; }; // shared_ptr 복사
} // x의 참조 카운트 유지
int main() {
auto lambda = createLambda();
std::cout << lambda() << '\n'; // 42 (안전)
}
4. this 캡처
[this] vs [=] vs [*this]
class MyClass {
int value_ = 42;
public:
auto getLambda1() {
return [this]() { return value_; }; // this 포인터 캡처
}
auto getLambda2() {
return [=]() { return value_; }; // [=]는 암시적으로 this 캡처
}
auto getLambda3() {
return [*this]() { return value_; }; // C++17: 객체 전체 복사
}
};
MyClass obj;
auto lambda = obj.getLambda1(); // this 포인터 저장
// obj가 소멸되면 lambda는 댕글링!
안전한 패턴
class MyClass {
int value_ = 42;
public:
// ✅ 객체 전체 복사 (C++17)
auto getLambda() {
return [*this]() { return value_; }; // 객체 복사
}
// ✅ 필요한 멤버만 복사
auto getLambda2() {
int v = value_;
return [v]() { return v; }; // 값만 복사
}
};
5. 자주 나오는 에러 10가지
에러 1: 참조 캡처 후 변수 소멸
// ❌ 댕글링 참조
std::function<void()> func;
{
int x = 42;
func = [&x]() { std::cout << x << '\n'; };
} // x 소멸
func(); // ❌ 크래시
에러 2: 값 캡처 수정 시도
// ❌ const 람다
int x = 10;
auto lambda = [x]() {
x = 20; // 컴파일 에러
};
// error: cannot assign to a variable captured by copy in a non-mutable lambda
// ✅ mutable 추가
auto lambda2 = [x]() mutable {
x = 20; // OK
};
에러 3: 멤버 변수 캡처 실수
class MyClass {
int value_ = 42;
public:
auto getLambda() {
// ❌ value_를 직접 캡처 불가
// return [value_]() { return value_; }; // 컴파일 에러
// ✅ this 캡처 또는 복사
return [this]() { return value_; }; // this 포인터
// ✅ 또는 C++14 초기화 캡처
return [v = value_]() { return v; }; // 값 복사
}
};
에러 4: 초기화되지 않은 캡처
// ❌ 초기화 안 된 변수 캡처
int x; // 초기화 안 함
auto lambda = [x]() { return x; }; // 쓰레기 값 캡처
// ✅ 초기화 후 캡처
int x = 42;
auto lambda = [x]() { return x; };
에러 5: std::move 캡처 실수
// ❌ 참조 캡처 후 move
std::unique_ptr<int> ptr = std::make_unique<int>(42);
auto lambda = [&ptr]() { // 참조 캡처
auto p = std::move(ptr); // ptr을 이동
};
lambda();
std::cout << *ptr << '\n'; // ❌ ptr은 이제 nullptr
// ✅ 초기화 캡처로 이동 (C++14)
auto lambda2 = [ptr = std::move(ptr)]() { // ptr을 람다로 이동
// ...
};
// 원본 ptr은 이제 nullptr (의도된 동작)
에러 6: 임시 객체 참조 캡처
// ❌ 임시 객체 참조 캡처
auto lambda = [&s = std::string("Hello")]() { // 임시 객체
return s;
}; // 임시 객체 소멸
std::cout << lambda() << '\n'; // ❌ 댕글링 참조
// ✅ 값 캡처
auto lambda2 = [s = std::string("Hello")]() { // 복사
return s;
};
에러 7: 비동기 실행 시 참조 캡처
// ❌ 스레드에서 참조 캡처
void foo() {
int x = 42;
std::thread t([&x]() { // x를 참조 캡처
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << x << '\n'; // ❌ x는 이미 소멸
});
t.detach(); // 스레드 분리
} // x 소멸
// ✅ 값 캡처
void foo() {
int x = 42;
std::thread t([x]() { // x를 복사
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << x << '\n'; // 안전
});
t.detach();
}
에러 8: 람다를 반환 시 참조 캡처
// ❌ 참조 캡처 후 반환
auto createLambda(int& x) {
return [&x]() { return x; }; // x를 참조 캡처
}
int main() {
int y = 42;
auto lambda = createLambda(y);
// y가 스코프를 벗어나면 lambda는 댕글링
}
// ✅ 값 캡처
auto createLambda(int x) {
return [x]() { return x; }; // x를 복사
}
에러 9: 제네릭 람다 타입 추론 실수
// ❌ 타입 추론 실패
auto lambda = {
return x + y;
};
struct NoAdd {};
lambda(NoAdd{}, NoAdd{}); // 컴파일 에러: operator+ 없음
// error: invalid operands to binary expression ('NoAdd' and 'NoAdd')
// ✅ Concepts로 제약 (C++20)
auto lambda2 = []<typename T>(T x, T y) requires requires { x + y; } {
return x + y;
};
에러 10: 캡처 리스트 오타
// ❌ 오타
int x = 10, y = 20;
auto lambda = [x, z]() { // z는 없음
return x + y;
};
// error: 'z' in capture list does not name a variable
실전 사례 분석
사례 1: 이벤트 핸들러
요구사항: 버튼 클릭 시 콜백 실행.
class Button {
std::function<void()> onClick_;
public:
void setOnClick(std::function<void()> callback) {
onClick_ = callback;
}
void click() {
if (onClick_) onClick_();
}
};
// ❌ 댕글링 참조
void setupButton(Button& btn) {
int counter = 0;
btn.setOnClick([&counter]() { // counter 참조 캡처
++counter;
std::cout << "Clicked " << counter << " times\n";
});
} // counter 소멸 → 람다는 댕글링
// ✅ shared_ptr로 수명 연장
void setupButton(Button& btn) {
auto counter = std::make_shared<int>(0);
btn.setOnClick([counter]() { // shared_ptr 복사
++(*counter);
std::cout << "Clicked " << *counter << " times\n";
});
}
사례 2: 비동기 작업
// ❌ 참조 캡처 (위험)
void processAsync(const std::vector<int>& data) {
std::thread t([&data]() { // data 참조 캡처
for (int x : data) {
process(x);
}
});
t.detach();
} // data 소멸 → 스레드는 댕글링 참조
// ✅ 값 캡처 (안전)
void processAsync(const std::vector<int>& data) {
std::thread t([data]() { // data 복사
for (int x : data) {
process(x);
}
});
t.detach();
}
// ✅ 또는 shared_ptr
void processAsync(std::shared_ptr<std::vector<int>> data) {
std::thread t([data]() { // shared_ptr 복사
for (int x : *data) {
process(x);
}
});
t.detach();
}
정리
람다 캡처 선택 가이드
| 상황 | 권장 캡처 | 이유 |
|---|---|---|
| 짧은 수명 | [=] 값 | 안전 |
| 긴 수명 | [=] 값 또는 shared_ptr | 댕글링 방지 |
| 큰 객체 | [&] 참조 (수명 주의) | 복사 비용 |
| 멤버 변수 | [*this] (C++17) | 객체 복사 |
| 비동기 | [=] 값 | 안전 |
| 즉시 실행 | [&] 참조 | 빠름 |
캡처 에러 방지 체크리스트
- 참조 캡처한 변수가 람다보다 오래 사는가?
- 비동기 실행 시 값 캡처를 사용하는가?
- 멤버 변수는 [*this]로 캡처하는가? (C++17)
- mutable이 필요한가?
- std::move 캡처는 초기화 캡처를 사용하는가?
핵심 규칙
- 기본은 값 캡처 [=] (안전)
- 참조 캡처는 수명 주의 (즉시 실행만)
- 비동기는 값 캡처 또는 shared_ptr
- *멤버 변수는 [this] (C++17)
- AddressSanitizer로 검증
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 람다 기초 | lambda 완벽 가이드
- C++ 람다 고급 | 제네릭 람다·초기화 캡처
- C++ 클로저 | 람다와 함수 객체
- C++ std::function vs 템플릿 | 성능 비교
마치며
람다 캡처는 편리하지만 위험합니다. 특히 참조 캡처는 댕글링 참조를 일으키기 쉽습니다.
핵심 원칙:
- 기본은 값 캡처 (안전)
- 참조 캡처는 즉시 실행만
- 비동기는 값 캡처 또는 shared_ptr
- AddressSanitizer로 검증
람다를 저장하거나 비동기 실행할 때는 값 캡처를 사용하세요. 참조 캡처는 즉시 실행하는 STL 알고리즘에서만 안전합니다.
다음 단계: 람다를 이해했다면, C++ 함수 객체와 람다에서 더 깊이 배워보세요.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |