C++ 초기화 캡처 | C++14 init-capture, move·unique_ptr 패턴 완전 정리
이 글의 핵심
초기화 캡처는 람다 클로저 안에 “이름 = 표현식”으로 멤버를 만들고, 특히 move로 소유권을 옮길 때 필수입니다. C++11 대비 차이와 실무 패턴을 정리합니다.
초기화 캡처(init-capture)란?
C++14부터 람다의 캡처 목록에 이름 = 표현식 형태를 쓸 수 있습니다. 이를 초기화 캡처(init-capture)라고 부릅니다. 클로저 객체 안에 지정한 이름의 멤버를 만들고, 오른쪽 표현식의 결과로 초기화합니다.
int factor = 10;
auto f = [factor = factor * 2]() { return factor; }; // 멤버 factor는 20으로 초기화
C++11에서는 [factor]처럼 외부 변수를 그대로 복사/참조만 할 수 있었고, 캡처 시점에 다른 표현식으로 값을 만들어 넣는 것은 불가능했습니다(별도 지역 변수를 두어야 했음).
C++11 캡처 vs C++14 초기화 캡처
C++11: 기본 캡처
| 문법 | 의미 |
|---|---|
[x] | 외부 x를 복사 |
[&x] | 외부 x를 참조 캡처 |
[=] | 기본 복사 캡처 |
[&] | 기본 참조 캡처 |
한계 예시: “외부 x의 값을 읽어서 2배한 값만 클로저에 넣고 싶다”면 C++11에서는 임시 변수가 필요합니다.
int x = 5;
int doubled = x * 2;
auto f = [doubled]() { return doubled; };
C++14: 초기화 캡처로 한 줄화
int x = 5;
auto f = [value = x * 2]() { return value; };
**캡처 목록의 이름**은 클로저 내부 스코프의 이름이고, = 오른쪽은 람다가 정의되는 시점에 평가됩니다.
정리
- C++11: 외부 이름을 그대로 복사/참조만 가능.
- C++14: 새 이름으로 임의의 표현식 결과를 멤버에 저장 가능(복사·이동·임시 객체 생성 포함).
Move 캡처 패턴
이동만 가능한 자원(예: unique_ptr, thread, 일부 파일 핸들)을 람다에 넣으려면 **복사 캡처 [ptr]는 불가능하고, 참조만 [&ptr]**로 두면 수명 문제가 생기기 쉽습니다. 이때 초기화 캡처로 소유권을 클로저 안으로 옮깁니다.
auto ptr = std::make_unique<int>(42);
auto work = [p = std::move(ptr)]() {
// p는 unique_ptr 멤버, 외부 ptr은 비워짐
return *p;
};
// ptr은 nullptr
패턴 요약
- 이름 충돌을 피하려면
[p = std::move(ptr)]처럼 캡처 안의 새 이름p를 씁니다. - 같은 이름을 유지하려면 (일부 컴파일러/스타일)
[ptr = std::move(ptr)]처럼 **외부ptr과 캡처 멤버ptr**이 그림자처럼 구분됩니다. 표준적으로는 초기화 캡처의 왼쪽이 람다의 멤버 이름입니다.
std::vector<int> v = big_vector();
auto f = [vec = std::move(v)]() mutable {
vec.push_back(1); // 클로저가 vector 소유
};
std::async나 std::thread에 넘길 람다에서 대용량 컨테이너를 복사하지 않고 넘기고 싶을 때 자주 씁니다.
unique_ptr 캡처
소유권을 람다가 가져가는 전형적인 형태입니다.
std::unique_ptr<Foo> foo = ...;
std::thread t([f = std::move(foo)]() {
f->doWork();
});
주의할 점
foo는 이동 후 비어 있음 — 이후foo를 쓰면 안 됩니다.- 람다를 복사할 수 있는지:
unique_ptr를 캡처한 람다는 복사 생성이 막힐 수 있어std::move로만 전달하는 경우가 많습니다.
auto task = [p = std::move(ptr)]() { /* ... */ };
std::async(std::launch::async, std::move(task));
shared_ptr를 캡처할 때는 복사 캡처 [p]도 가능하지만, 비용과 수명 공유 의미를 구분해 선택합니다.
실전 예제
예제 1: 비동기 작업에 리소스 이동
void startBackground(std::unique_ptr<Connection> conn) {
std::thread([c = std::move(conn)]() mutable {
c->run();
}).detach();
}
예제 2: optional / 상태 플래그와 조합
auto make_handler(std::optional<int> limit) {
return [lim = std::move(limit)](int x) {
if (lim && x > *lim) return false;
return true;
};
}
예제 3: C++14 이전과의 대비
// C++11 스타일 (보조 변수)
std::vector<int> v = ...;
std::vector<int> v_copy = std::move(v);
auto f = [v_copy]() { return v_copy.size(); };
// C++14 (한 클로저 정의 안에서)
std::vector<int> v = ...;
auto f = [vec = std::move(v)]() { return vec.size(); };
[*this] / [=, *this] (C++17)
*this 캡처는 클래스 멤버 함수 안의 람다에서 현재 객체의 복사본을 저장할 때 씁니다. “참조로 this만 잡아 두었다가 객체 수명이 끝나는” 실수를 줄입니다.
struct S {
int n = 0;
auto make_lambda() {
return [*this]() { return n; }; // S의 복사본이 클로저에 저장
}
};
초기화 캡처([n = x])와 목적이 비슷하게 값 스냅샷을 만든다는 점에서 같이 이해하면 좋습니다.
흔한 실수
1. 이동 후 외부 변수 재사용
auto p = std::make_unique<int>(1);
auto f = [q = std::move(p)]() { return *q; };
// *p // 정의되지 않음 또는 assert 실패 — 사용 금지
2. 참조 캡처와 초기화 캡처 혼동
초기화 캡처 **[x = expr]**의 expr는 정의 시점에 한 번 평가됩니다. 외부 변수를 계속 추적하려면 [&x] 또는 [=]의 의미를 써야 하고, 스냅샷이 필요할 때 초기화 캡처가 맞습니다.
3. 기본 캡처와 함께 쓰는 규칙
[=, x = expr]처럼 기본 복사 + 특정 항목만 초기화 캡처를 섞을 수 있습니다. 같은 이름이 두 번 나오면 안 되고, 초기화 캡처 항목은 기본 캡처와 독립적으로 멤버를 만듭니다. 팀 컨벤션에 따라 [=]/[&] 남용을 피하고 필요한 것만 명시하는 편이 리뷰에 유리합니다.
4. 수명: 참조로 캡처한 대상
초기화 캡처로 참조를 저장하는 것도 문법상 가능하지만(구현·버전에 따라 주의), 댕글링 위험이 큽니다. 스택 프레임이 끝난 뒤 호출되는 람다는 **값으로 소유권을 옮기거나 shared_ptr**를 검토하세요.
5. mutable 누락
복사 캡처된 멤버를 람다 본문에서 수정하려면 **mutable**이 필요합니다(일반 [=]와 동일).
auto counter = [n = 0]() mutable { return ++n; };
요약
| 주제 | 요지 |
|---|---|
| C++11 | [x], [&x], [=], [&] |
| C++14 | [name = expr], [name = std::move(x)] |
| move | 이동 전용 타입·대용량 데이터를 스레드/비동기로 넘길 때 |
| 실수 | 이동 후 원본 사용, 참조 수명, mutable |
관련 글: 람다 캡처 상세, make_unique, 커스텀 삭제자.
같이 보면 좋은 글 (내부 링크)
- C++ 람다 캡처
- C++ make_unique & make_shared
- C++ 람다·캡처 관련 오류
관련 글
- C++ 람다 완전 정리
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트