[2026] C++ 초기화 캡처 | C++14 init-capture, move·unique_ptr 패턴 완전 정리
이 글의 핵심
C++11 단순 캡처와 C++14 초기화 캡처([x = expr])의 차이, move 캡처·unique_ptr 넘기기, 실전 예제와 흔한 실수(수명·중복 이름)까지 정리합니다.
초기화 캡처(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]로 두면 수명 문제가 생기기 쉽습니다. 이때 초기화 캡처로 소유권을 클로저 안으로 옮깁니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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이 그림자처럼 구분됩니다. 표준적으로는 초기화 캡처의 왼쪽이 람다의 멤버 이름입니다. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::vector<int> v = big_vector();
auto f = [vec = std::move(v)]() mutable {
vec.push_back(1); // 클로저가 vector 소유
};
std::async나 std::thread에 넘길 람다에서 대용량 컨테이너를 복사하지 않고 넘기고 싶을 때 자주 씁니다.
unique_ptr 캡처
소유권을 람다가 가져가는 전형적인 형태입니다. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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: 비동기 작업에 리소스 이동
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void startBackground(std::unique_ptr<Connection> conn) {
std::thread([c = std::move(conn)]() mutable {
c->run();
}).detach();
}
예제 2: optional / 상태 플래그와 조합
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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 이전과의 대비
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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만 잡아 두었다가 객체 수명이 끝나는” 실수를 줄입니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
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, 커스텀 삭제자. |