C++ 초기화 캡처 | C++14 init-capture, move·unique_ptr 패턴 완전 정리

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

패턴 요약

  1. 이름 충돌을 피하려면 [p = std::move(ptr)]처럼 캡처 안의 새 이름 p를 씁니다.
  2. 같은 이름을 유지하려면 (일부 컴파일러/스타일) [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::asyncstd::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) 핵심 문법 치트시트