본문으로 건너뛰기
Previous
Next
C++ 초기화 캡처 | C++14 init-capture, move·unique_ptr 패턴 완전 정리

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

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

이 글의 핵심

C++14 초기화 캡처([x = expr])의 이동 의미·클로저 타입 멤버 생성·완벽 전달·캡처 메모리 레이아웃·프로덕션 패턴을 내부 관점에서 정리합니다. 기본 문법·unique_ptr·흔한 실수와 함께 읽으면 됩니다.

초기화 캡처(init-capture)란?

C++14부터 람다의 캡처 목록이름 = 표현식 형태를 쓸 수 있습니다. 이를 초기화 캡처(init-capture)라고 부릅니다. 클로저 객체 안에 지정한 이름의 멤버를 만들고, 오른쪽 표현식의 결과로 초기화합니다.

기본 문법과 동작 원리

int factor = 10;
auto f = [factor = factor * 2]() { return factor; };  // 멤버 factor는 20으로 초기화
std::cout << f() << std::endl;  // 출력: 20
std::cout << factor << std::endl;  // 출력: 10 (외부 변수는 그대로)

내부 동작 원리:

초기화 캡처는 컴파일러가 다음과 같은 클로저 클래스를 생성합니다:

// 컴파일러가 생성하는 대략적인 클로저 클래스
class __lambda_closure {
private:
    int factor;  // 캡처된 멤버 변수
    
public:
    __lambda_closure(int init_factor) : factor(init_factor) {}
    
    auto operator()() const {
        return factor;  // 멤버 변수 사용
    }
};

// 실제 람다 생성
int factor = 10;
__lambda_closure f(factor * 2);  // factor * 2가 평가되어 생성자에 전달

중요한 점:

  1. 캡처 목록의 왼쪽 이름(factor)은 클로저 객체의 멤버 변수 이름
  2. 오른쪽 표현식(factor * 2)은 람다 정의 시점에 즉시 평가
  3. 외부 변수(factor)와 캡처된 멤버(factor)는 별개의 변수 (이름만 같음)

왜 초기화 캡처가 필요한가?

C++11에서는 [factor]처럼 외부 변수를 그대로 복사/참조만 할 수 있었고, 캡처 시점에 다른 표현식으로 값을 만들어 넣는 것은 불가능했습니다(별도 지역 변수를 두어야 했음).

실무 문제 상황:

// 문제 1: 변환된 값을 캡처하고 싶을 때 (C++11)
int celsius = 25;
int fahrenheit = celsius * 9 / 5 + 32;  // 임시 변수 필요
auto print_temp = [fahrenheit]() {
    std::cout << fahrenheit << "°F" << std::endl;
};

// 문제 2: 여러 람다에서 같은 패턴 반복
int value = 100;
int doubled = value * 2;
auto f1 = [doubled]() { return doubled; };
int tripled = value * 3;
auto f2 = [tripled]() { return tripled; };

// C++14로 간결하게 해결
auto print_temp = [f = celsius * 9 / 5 + 32]() {
    std::cout << f << "°F" << std::endl;
};

auto f1 = [doubled = value * 2]() { return doubled; };
auto f2 = [tripled = value * 3]() { return tripled; };

실무에서의 장점:

  1. 코드 간결성: 임시 변수 제거
  2. 의도 명확성: 캡처 목록만 보고도 어떤 값이 저장되는지 파악
  3. 스코프 오염 방지: 람다 밖에 불필요한 변수를 두지 않음
  4. 이동 의미론 지원: unique_ptr 같은 이동 전용 타입 처리 가능

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]로 두면 수명 문제가 생기기 쉽습니다. 이때 초기화 캡처로 소유권을 클로저 안으로 옮깁니다.

기본 Move 캡처

auto ptr = std::make_unique<int>(42);
auto work = [p = std::move(ptr)]() {
    // p는 unique_ptr 멤버, 외부 ptr은 비워짐
    std::cout << "값: " << *p << std::endl;
    return *p;
};

work();  // 정상 실행

// 주의: ptr은 이제 nullptr
if (ptr == nullptr) {
    std::cout << "ptr은 이동되어 비었습니다" << std::endl;
}

// ptr을 다시 사용하면 정의되지 않은 동작!
// *ptr;  // 크래시!

내부에서 무슨 일이 일어나나요?

// 컴파일러가 생성하는 클로저 클래스 (개념적)
class __lambda_move {
private:
    std::unique_ptr<int> p;  // 이동된 unique_ptr 저장
    
public:
    // 생성자에서 이동 생성
    __lambda_move(std::unique_ptr<int>&& init_p) 
        : p(std::move(init_p)) {}
    
    auto operator()() const {
        std::cout << "값: " << *p << std::endl;
        return *p;
    }
};

// 실제 사용
auto ptr = std::make_unique<int>(42);
__lambda_move work(std::move(ptr));  // ptr의 소유권이 클로저로 이동

왜 Move가 필요한가? (복사 vs 참조 vs 이동)

// 문제 1: 복사 캡처 - 컴파일 에러!
auto ptr = std::make_unique<int>(42);
// auto bad = [ptr]() { return *ptr; };  
// 에러: unique_ptr는 복사 생성자가 삭제됨

// 문제 2: 참조 캡처 - 댕글링 위험!
auto make_dangerous_lambda() {
    auto ptr = std::make_unique<int>(42);
    return [&ptr]() { return *ptr; };  // 위험!
}
// ptr은 함수 종료 시 파괴됨 → 람다는 댕글링 참조

// 해결: Move 캡처 - 안전한 소유권 이전
auto make_safe_lambda() {
    auto ptr = std::make_unique<int>(42);
    return [p = std::move(ptr)]() { return *p; };  // 안전!
}
// 람다가 ptr의 소유권을 가져감

실무 시나리오: 비동기 작업

// 나쁜 예: 참조 캡처 + 비동기
void bad_async_example() {
    std::vector<int> data = load_large_data();  // 1GB 데이터
    
    // 위험: data가 함수 종료 시 파괴됨
    std::async(std::launch::async, [&data]() {
        process_data(data);  // 댕글링 참조!
    });
}  // data 파괴 → 비동기 작업에서 크래시

// 좋은 예: Move 캡처
void good_async_example() {
    std::vector<int> data = load_large_data();
    
    // 안전: 소유권이 람다로 이동
    std::async(std::launch::async, [data = std::move(data)]() {
        process_data(data);  // 안전!
    });
}  // 원본 data는 비었지만, 람다가 소유권을 가짐

이름 패턴: 새 이름 vs 같은 이름

패턴 1: 새 이름 사용 (권장)

auto ptr = std::make_unique<int>(42);
auto work = [p = std::move(ptr)]() {  // p는 새 이름
    return *p;
};

// 장점: 
// - 외부 변수(ptr)와 캡처 변수(p)가 명확히 구분됨
// - 이동 후 ptr 사용 실수 방지
// - 코드 리뷰 시 의도가 명확함

패턴 2: 같은 이름 사용 (주의 필요)

auto ptr = std::make_unique<int>(42);
auto work = [ptr = std::move(ptr)]() {  // 같은 이름 ptr
    return *ptr;
};

// 주의점:
// - 왼쪽 ptr: 클로저의 멤버 변수
// - 오른쪽 ptr: 외부 변수
// - 이름이 같아서 섀도잉 발생
// - 외부 ptr은 이동 후 nullptr이지만, 이름이 같아 혼란 가능

실전 권장 사항:

// 1. 짧은 스코프에서는 새 이름
{
    auto ptr = std::make_unique<Resource>();
    auto task = [res = std::move(ptr)]() {
        res->use();
    };
}

// 2. 여러 변수를 이동할 때는 접두사 사용
{
    auto conn = std::make_unique<Connection>();
    auto cache = std::make_unique<Cache>();
    
    auto worker = [
        conn_ = std::move(conn),
        cache_ = std::move(cache)
    ]() {
        conn_->query();
        cache_->store();
    };
}

대용량 컨테이너 Move

// 실무 예제: 대용량 벡터 처리
std::vector<int> generate_data() {
    std::vector<int> v(10'000'000);  // 40MB
    std::iota(v.begin(), v.end(), 0);
    return v;
}

void process_in_thread() {
    auto data = generate_data();
    
    // 나쁜 예: 복사 캡처 (40MB 복사!)
    // std::thread t([data]() {  // 40MB 복사 발생
    //     for (int x : data) { /* ... */ }
    // });
    
    // 좋은 예: Move 캡처 (포인터 이동만)
    std::thread t([vec = std::move(data)]() {
        for (int x : vec) {
            // 복사 없이 원본 데이터 사용
        }
    });
    
    t.detach();
    // data는 비었지만, 스레드가 소유권을 가짐
}

성능 비교:

#include <chrono>

void benchmark_copy_vs_move() {
    std::vector<int> large_data(10'000'000);
    
    // 복사 캡처 (느림)
    auto start = std::chrono::high_resolution_clock::now();
    auto copy_lambda = [data = large_data]() {  // 복사 발생
        return data.size();
    };
    auto end = std::chrono::high_resolution_clock::now();
    auto copy_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    // Move 캡처 (빠름)
    start = std::chrono::high_resolution_clock::now();
    auto move_lambda = [data = std::move(large_data)]() {  // 이동만
        return data.size();
    };
    end = std::chrono::high_resolution_clock::now();
    auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    std::cout << "복사: " << copy_time.count() << "μs\n";  // ~20000μs
    std::cout << "이동: " << move_time.count() << "μs\n";  // ~1μs
    std::cout << "속도 향상: " << (copy_time.count() / move_time.count()) << "배\n";
}

std::asyncstd::thread에 넘길 람다에서 대용량 컨테이너를 복사하지 않고 넘기고 싶을 때 자주 씁니다.

내부 동작 심화: 이동 의미·클로저 타입·전달·메모리·프로덕션

이 절에서는 문법을 넘어, 표준 초기화 규칙·클로저 타입 생성 모델·전달 의미론·객체 표현(메모리)·현업에서의 타입 소거 한계를 한데 묶어 설명합니다. 컴파일러마다 세부 구현은 다르지만, 관찰 가능한 동작과 이식성에 영향을 주는 축은 공통입니다.

1) init-capture와 이동 의미론(오버로드 해석)

초기화 캡처 [name = expr]의 오른쪽 expr은 람다가 정의되는 지점에서 평가되고, 그 결과로 클로저의 데이터 멤버 name이 만들어집니다. 이 초기화는 개념적으로 T name(expr); 형태의 직접 초기화에 가깝습니다(정확한 표준 용어는 [expr.prim.lambda]의 init-capture 규정을 따릅니다).

핵심은 “표현식 카테고리(expr의 값/좌값/소멸값) → T의 어떤 생성자가 선택되는가”입니다.

#include <utility>
#include <vector>

void demo_move_vs_copy() {
    std::vector<int> v{1, 2, 3};

    // (A) 복사: expr이 좌값 lvalue → T(const T&) 또는 T(T&) 쪽이 후보
    auto fa = [x = v]() { return x.size(); };

    // (B) 이동: std::move(v)는 xvalue → T(T&&)가 후보
    auto fb = [x = std::move(v)]() { return x.size(); };

    // (C) 임시: prvalue는 보통 복사/이동을 “개념적으로” 우회(복사 생략)하거나
    //    최종적으로 이동 초기화로 귀결되는 경우가 많음(표준/컴파일러 버전에 따름)
    auto fc = [x = std::vector<int>{9, 8, 7}]() { return x[0]; };
}

실무에서 자주 놓치는 점은 다음과 같습니다.

  1. std::move는 이름을 “이동”하지 않습니다. 이름 v는 여전히 std::vector<int>이며, std::move(v)이동 생성자에 바인딩되기 쉬운 카테고리로 캐스팅한 값입니다. 이후 v를 건드리면 이동 후 상태(use-after-move) 이슈로 이어집니다.
  2. [x = v]가 복사인지 이동인지는 expr이 좌값인지 여부에 크게 좌우됩니다. “캡처라서 자동 이동”이 아닙니다.
  3. noexcept 이동std::vector 같은 컨테이너의 강한 예외 안전 보장(예: 재할당 시 이동만 시도)과 맞물립니다. 이동 생성자가 noexcept가 아니면 상황에 따라 복사로 되돌아가는 경로가 생길 수 있습니다(특히 표준 라이브러리 내부 최적화와 연관).

요약하면, init-capture는 “캡처”라는 문법 설탕을 씌운 멤버 직접 초기화이며, 이동 의미론은 평범한 클래스 멤버 초기화와 같은 규칙으로 읽는 것이 가장 안전합니다.

2) 클로저 타입과 멤버 생성(개념적 모델)

람다 표현식은 고유한 클로저 타입(unnamed closure type) 을 만듭니다. 단순 캡처 [a], 참조 캡처 [&a], 초기화 캡처 [x = expr]는 모두 이 타입의 비정적 데이터 멤버(non-static data member) 로 대응시킬 수 있습니다.

// 아래는 “개념적” 의사 코드입니다(이름·정확한 시그니처는 구현별).
struct /* unnamed */ __lambda_1 {
    std::vector<int> x;  // [x = std::move(v)] 에 대응

    template<class... Args>
    auto operator()(Args&&... args) const {  // mutable이 아니면 const
        // 본문
        return /* ... */;
    }
};

복사/이동 가능성은 멤버의 복사/이동 가능성을 그대로 따릅니다.

  • std::unique_ptr<T> 멤버를 두면 복사 생성자가 삭제되는 전형적 패턴이 되고, 클로저 객체도 복사 불가·이동만 가능해집니다.
  • 클로저를 std::function 같은 복사 가능(type-erased) 컨테이너에 넣으려면, 결국 내부 저장 타입이 복사 가능해야 하므로 고정 크기 소형 저장소(small buffer) + 복제 가능 조건이 걸립니다. 이것이 “unique_ptr 캡처 람다는 std::function에 안 들어간다”의 본질입니다.

멤버 순서는 표준이 구현에 맡기는 부분이 있어 이식성 있게 순서에 의존하는 코드는 금물입니다. 다만 동일 번역 단위·동일 람다 표현식에 대해서는 한 구현체가 안정적으로 동일 레이아웃을 유지하는 것이 일반적입니다.

mutable 람다는 operator()()비-const가 되어 멤버를 수정할 수 있습니다. 이는 “값 캡처가 const 멤버처럼 보이지만 실제론 객체 상태를 바꿀 수 있다”는 사용자 혼동을 줄이려면, 스레드 공유 mutable 캡처는 특히 주의해야 합니다.

3) 완벽 전달(perfect forwarding)과 init-capture

제네릭 람다(템플릿 operator())에서 인자를 클로저 내부로 그대로 옮기되, 바깥에서 받은 전달 참조(forwarding reference) 를 올바르게 처리하려면 init-capture가 강력합니다.

#include <utility>
#include <vector>

// 전달 참조(T&&)를 캡처 시점에 한 번만 forward → 멤버 `v`로 “고정”
// (이동 전용 타입은 이 패턴 그대로 쓰기 어렵고, 보통 바깥에서 std::move로 넘깁니다)
template <class T>
auto wrap_vector(T&& t) {
    return [v = std::forward<T>(t)]() {
        return v.size();
    };
}

void demo_forwarding() {
    std::vector<int> a{1, 2, 3};
    auto f1 = wrap_vector(a);              // 좌값: 보통 복사 초기화 경로
    auto f2 = wrap_vector(std::vector<int>{9, 8});  // prvalue: 복사 생략/이동 등(구현에 따름)
    (void)f1; (void)f2;
}

주의 깊게 봐야 할 규칙은 다음과 같습니다.

  1. std::forward<T>(x)는 “아직 전달할 값”이 존재할 때만 의미가 완결됩니다. init-capture는 그 지점에서 멤버 초기화로 끝나므로, 이후 바깥 스코프의 x를 다시 forward한다고 생각하면 논리가 깨집니다.
  2. 두 번 이동하려면 멤버를 std::move로 소비해야 하며, 그 순간부터는 동일 멤버 재사용 금지가 일반적입니다.
  3. C++20의 init-capture pack(...)을 쓰면 가변 인자를 멤버 튜플처럼 저장하는 패턴이 가능합니다. 다만 템플릿 오류 메시지가 길어지기 쉬우므로 래퍼 구조체/튜플로 의미를 이름 붙이는 편이 팀 생산성에 유리한 경우가 많습니다.

실무에서는 “forward로 받았으니 끝”이 아니라, 저장 수명(누가 소유하는가)예외 안전(부분 이동) 까지 같이 설계해야 합니다.

4) 캡처 변수의 메모리 레이아웃과 관찰 포인트

클로저 객체의 크기는 대개 캡처 멤버들의 합 + 정렬 패딩(alignment) 으로 결정됩니다. 표준은 람다의 메모리 배치를 세밀하게 고정하지 않으므로, sizeof로 성능을 “추측”하기보다는 프로파일링이 우선입니다.

#include <cstdint>
#include <iostream>

int main() {
    int a = 1;
    double b = 2.0;
    auto lam = [a, b]() { return a + (int)b; };
    std::cout << sizeof(lam) << '\n';  // 플랫폼/정렬에 따라 다름
}

도움이 되는 직관은 다음과 같습니다.

  • 참조 캡처는 개념적으로 포인터 크기의 저장을 수반하는 경우가 많습니다(구현 세부는 다를 수 있음).
  • 빈 캡처 람다는 sizeof가 1인 경우가 많습니다(“완전 빈 타입”도 C++ 객체는 주소를 가져야 할 수 있어서).
  • 정렬 요구가 큰 멤버가 끼면 패딩이 생겨 작은 멤버를 앞에 둔다고 항상 작아지지는 않습니다.(멤버 순서 미지정이면 이 이야기 자체가 위험)

ABI 관점에서 람다 타입은 보통 번역 단위 내부 타입처럼 취급되지만, 템플릿 인스턴스화 경계를 넘겨 람다 타입을 노출하는 설계는 피하는 것이 안전합니다.

5) 프로덕션에서의 람다 패턴(타입 소거·이동 전용·경계)

현업에서는 init-capture가 특히 다음에서 빛납니다: 비동기 경계(스레드/큐/IO 서비스) 로 “소유권만 넘기고”, 실행 지점에서는 복사 가능한 핸들만 바깥에 남기고 싶을 때입니다.

패턴 A — std::packaged_task로 이동 전용 작업을 타입 소거

std::function 대신, 이동만 가능한 작업을 std::packaged_task<R()> 같은 형태로 감싸면 “복사 불가 람다”를 큐에 넣는 문제를 피하는 경우가 많습니다(프로젝트별로는 asio::post + 커스텀 move-only wrapper를 쓰기도 합니다).

#include <future>
#include <memory>
#include <thread>
#include <utility>

std::future<int> launch_move_only(std::unique_ptr<int> p) {
    std::packaged_task<int()> task{
        [owned = std::move(p)]() mutable {
            return *owned * 2;
        }
    };
    auto fut = task.get_future();
    std::thread(std::move(task)).detach();
    return fut;
}

패턴 B — 작업 큐: “외부는 shared_ptr, 내부 실행은 unique_ptr로 승격”

경계에서만 공유가 필요하면 shared_from_this 패턴과 결합하고, 워커로 넘길 때 init-capture로 독점 소유로 바꿔 레퍼런스 카운팅을 끊습니다.

패턴 C — 코루틴/스레드 풀 전단: stop_token·조건 변수와 섞지 않기

캡처에 동기화 프리미티브 자체를 넣기보다, 소유 자원 + 불변 설정을 넣고 공유 상태는 명시적 타입으로 두는 편이 디버깅에 유리합니다. mutable 카운터를 여러 스레드가 호출하면 레이스가 생깁니다.

패턴 D — 관측 가능성: move 후 nullptr/empty() 강제

프로덕션 코드에서는 이동 직후 원본 컨테이너/스마트 포인터를 즉시 유효 범위를 줄이거나, 스코프를 쪼개 실수 사용을 컴파일 타임에 구조적으로 불가능하게 만드는 편이 안전합니다.

이 다섯 가지(초기화 규칙·클로저 멤버 모델·forward의 일회성·레이아웃의 비고정성·move-only 경계 패턴)를 염두에 두면, init-capture는 단순한 문법 설탕이 아니라 자원 수명을 클로저 객체에 귀속시키는 설계 도구로 정확히 쓸 수 있습니다.


unique_ptr 캡처 상세 가이드

소유권을 람다가 가져가는 전형적인 형태입니다. unique_ptr는 복사가 불가능하므로, 초기화 캡처로 이동해야 합니다.

기본 패턴

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " 생성\n";
    }
    
    ~Resource() {
        std::cout << "Resource " << id_ << " 소멸\n";
    }
    
    void process() {
        std::cout << "Resource " << id_ << " 처리 중\n";
    }

private:
    int id_;
};

void thread_example() {
    auto resource = std::make_unique<Resource>(1);
    
    std::thread t([res = std::move(resource)]() {
        res->process();
        // 스레드가 종료되면 res가 자동 삭제됨
    });
    
    t.join();
    
    // resource는 nullptr
    if (resource == nullptr) {
        std::cout << "원본 resource는 비어있음\n";
    }
}

// 출력:
// Resource 1 생성
// Resource 1 처리 중
// Resource 1 소멸
// 원본 resource는 비어있음

주의사항 1: 이동 후 원본 사용 금지

void dangerous_pattern() {
    auto ptr = std::make_unique<int>(42);
    
    auto task = [p = std::move(ptr)]() {
        return *p;
    };
    
    // 위험! ptr은 이미 이동됨
    // if (*ptr > 0) { }  // 정의되지 않은 동작
    
    // 안전한 패턴: nullptr 체크
    if (ptr != nullptr) {
        // 이 블록은 실행되지 않음
        std::cout << *ptr << std::endl;
    } else {
        std::cout << "ptr이 이동되어 비었습니다\n";
    }
}

컴파일러 경고 활용:

// 일부 컴파일러는 이동 후 사용을 경고
auto ptr = std::make_unique<int>(42);
auto task = [p = std::move(ptr)]() {
    return *p;
};
auto value = *ptr;  // 경고: use-after-move

주의사항 2: 람다 복사 불가능

// unique_ptr를 캡처한 람다는 복사 불가능
void copy_lambda_issue() {
    auto ptr = std::make_unique<int>(42);
    
    auto task = [p = std::move(ptr)]() {
        return *p;
    };
    
    // 에러: 람다를 복사할 수 없음
    // auto task2 = task;  // 컴파일 에러!
    
    // 해결: std::move로 이동
    auto task2 = std::move(task);  // OK
    
    // 이제 task는 사용 불가
    // task();  // 정의되지 않은 동작
}

std::function과의 호환성 문제:

#include <functional>

void function_wrapper_issue() {
    auto ptr = std::make_unique<int>(42);
    
    // 에러: std::function은 복사 가능해야 함
    // std::function<int()> fn = [p = std::move(ptr)]() {
    //     return *p;
    // };
    
    // 해결 1: shared_ptr 사용
    auto shared = std::make_shared<int>(42);
    std::function<int()> fn1 = [p = shared]() {
        return *p;
    };
    
    // 해결 2: std::shared_ptr로 감싸기
    auto wrapped = std::make_shared<std::unique_ptr<int>>(
        std::make_unique<int>(42)
    );
    std::function<int()> fn2 = [p = wrapped]() {
        return **p;  // 이중 역참조
    };
}

실전 패턴: 비동기 작업

#include <future>
#include <thread>

// 패턴 1: std::async
void async_pattern() {
    auto data = std::make_unique<std::vector<int>>(1000000);
    
    // 람다를 std::move로 전달
    auto future = std::async(
        std::launch::async,
        [data = std::move(data)]() {
            // 비동기 작업
            return std::accumulate(data->begin(), data->end(), 0LL);
        }
    );
    
    auto result = future.get();
    std::cout << "합계: " << result << std::endl;
}

// 패턴 2: 여러 리소스 이동
void multiple_resources() {
    auto conn = std::make_unique<Connection>();
    auto cache = std::make_unique<Cache>();
    auto logger = std::make_unique<Logger>();
    
    std::thread worker([
        conn = std::move(conn),
        cache = std::move(cache),
        logger = std::move(logger)
    ]() {
        logger->log("작업 시작");
        auto data = conn->fetch();
        cache->store(data);
        logger->log("작업 완료");
    });
    
    worker.detach();
}

// 패턴 3: 조건부 이동
auto create_task(bool use_cache) {
    auto cache = use_cache ? 
        std::make_unique<Cache>() : 
        nullptr;
    
    return [cache = std::move(cache)]() {
        if (cache) {
            cache->use();
        } else {
            // 캐시 없이 작업
        }
    };
}

unique_ptr vs shared_ptr 선택 가이드

// unique_ptr: 독점 소유권 (권장)
void use_unique_ptr() {
    auto data = std::make_unique<Data>();
    
    // 장점:
    // - 오버헤드 최소 (참조 카운터 없음)
    // - 소유권 명확 (하나의 소유자만)
    // - 이동만 가능 (의도치 않은 복사 방지)
    
    auto task = [data = std::move(data)]() {
        data->process();
    };
    
    // 람다가 유일한 소유자
}

// shared_ptr: 공유 소유권 (필요시에만)
void use_shared_ptr() {
    auto data = std::make_shared<Data>();
    
    // 장점:
    // - 여러 람다에서 공유 가능
    // - 복사 가능 (std::function 호환)
    // - 자동으로 마지막 참조자가 삭제
    
    auto task1 = [data]() { data->process(); };
    auto task2 = [data]() { data->process(); };
    
    // 둘 다 같은 객체 참조
    
    // 단점:
    // - 참조 카운터 오버헤드 (원자적 연산)
    // - 순환 참조 위험
    // - 소유권이 불명확해질 수 있음
}

// 실무 선택 기준:
// 1. 기본은 unique_ptr 사용
// 2. 여러 곳에서 공유 필요 → shared_ptr
// 3. 비동기 작업 간 데이터 공유 → shared_ptr
// 4. 복사 불가능한 람다 허용 가능 → unique_ptr
// 5. std::function 필요 → shared_ptr

커스텀 삭제자와 함께 사용

// 파일 핸들 예제
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::cout << "파일 닫는 중\n";
            fclose(f);
        }
    }
};

using FilePtr = std::unique_ptr<FILE, FileDeleter>;

void file_lambda_example() {
    FilePtr file(fopen("data.txt", "r"), FileDeleter{});
    
    auto task = [f = std::move(file)]() {
        if (f) {
            char buffer[256];
            while (fgets(buffer, sizeof(buffer), f.get())) {
                std::cout << buffer;
            }
        }
        // 람다 종료 시 FileDeleter 자동 호출
    };
    
    std::thread t(std::move(task));
    t.join();
}

// 소켓 핸들 예제
void socket_lambda_example() {
    struct SocketDeleter {
        void operator()(int* sock) const {
            if (sock && *sock >= 0) {
                close(*sock);
                delete sock;
            }
        }
    };
    
    std::unique_ptr<int, SocketDeleter> socket(new int(create_socket()));
    
    auto network_task = [sock = std::move(socket)]() {
        send_data(*sock, "Hello");
        // 람다 종료 시 소켓 자동 닫힘
    };
    
    std::async(std::launch::async, std::move(network_task));
}

성능 고려사항:

// unique_ptr: 포인터 크기 (8 bytes on 64-bit)
sizeof(std::unique_ptr<int>);  // 8

// shared_ptr: 포인터 + 제어 블록 포인터 (16 bytes)
sizeof(std::shared_ptr<int>);  // 16

// 참조 카운터 업데이트 비용
void benchmark_ptr_types() {
    const int iterations = 10'000'000;
    
    // unique_ptr: 빠름 (참조 카운터 없음)
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        auto p = std::make_unique<int>(i);
        auto task = [p = std::move(p)]() {};
    }
    auto end = std::chrono::high_resolution_clock::now();
    auto unique_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    // shared_ptr: 느림 (원자적 참조 카운터 업데이트)
    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) {
        auto p = std::make_shared<int>(i);
        auto task = [p]() {};  // 복사 → 참조 카운터 증가
    }
    end = std::chrono::high_resolution_clock::now();
    auto shared_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "unique_ptr: " << unique_time.count() << "ms\n";
    std::cout << "shared_ptr: " << shared_time.count() << "ms\n";
}

실전 예제

예제 1: 데이터베이스 연결 풀 with 비동기 작업

#include <memory>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

class DatabaseConnection {
public:
    DatabaseConnection(const std::string& conn_str) 
        : conn_str_(conn_str) {
        std::cout << "DB 연결 생성: " << conn_str_ << std::endl;
    }
    
    ~DatabaseConnection() {
        std::cout << "DB 연결 종료: " << conn_str_ << std::endl;
    }
    
    void execute_query(const std::string& query) {
        std::cout << "쿼리 실행: " << query << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

private:
    std::string conn_str_;
};

// 비동기 작업에 DB 연결 이동
void execute_async_query(
    std::unique_ptr<DatabaseConnection> conn,
    const std::string& query
) {
    // 연결 소유권을 람다로 이동
    std::thread([
        conn = std::move(conn),  // DB 연결 캡처
        query                     // 쿼리 문자열 복사
    ]() {
        try {
            conn->execute_query(query);
            
            // 추가 처리
            std::cout << "쿼리 완료, 결과 처리 중...\n";
            
        } catch (const std::exception& e) {
            std::cerr << "쿼리 실행 실패: " << e.what() << std::endl;
        }
        
        // 람다 종료 시 conn 자동 삭제 → DB 연결 종료
    }).detach();
    
    // 이 시점에서 conn은 nullptr
    // 스레드가 DB 연결의 소유권을 가짐
}

// 사용 예시
void example_db_async() {
    auto conn = std::make_unique<DatabaseConnection>("localhost:5432");
    
    execute_async_query(
        std::move(conn),
        "SELECT * FROM users WHERE active = true"
    );
    
    // conn은 이미 이동됨
    // 메인 스레드는 계속 실행
    std::cout << "메인 스레드 계속 실행 중...\n";
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

예제 2: HTTP 요청 핸들러 with 상태 관리

#include <optional>
#include <functional>
#include <unordered_map>

class RequestContext {
public:
    std::unordered_map<std::string, std::string> headers;
    std::unordered_map<std::string, std::string> query_params;
    std::string body;
    
    std::optional<int> get_user_id() const {
        auto it = headers.find("X-User-ID");
        if (it != headers.end()) {
            try {
                return std::stoi(it->second);
            } catch (...) {
                return std::nullopt;
            }
        }
        return std::nullopt;
    }
};

// 인증 미들웨어 생성
auto make_auth_handler(std::optional<int> required_level) {
    // required_level을 람다에 이동 캡처
    return [level = std::move(required_level)](const RequestContext& ctx) {
        auto user_id = ctx.get_user_id();
        
        if (!user_id) {
            std::cout << "인증 실패: 사용자 ID 없음\n";
            return false;
        }
        
        // 권한 레벨 체크 (옵션)
        if (level) {
            int user_level = get_user_level(*user_id);
            if (user_level < *level) {
                std::cout << "권한 부족: 필요=" << *level 
                         << ", 현재=" << user_level << "\n";
                return false;
            }
        }
        
        std::cout << "인증 성공: 사용자 " << *user_id << "\n";
        return true;
    };
}

// 레이트 리미터 생성
auto make_rate_limiter(std::optional<int> max_requests) {
    return [
        limit = std::move(max_requests),
        count = 0  // 초기화 캡처로 카운터 생성
    ](const RequestContext& ctx) mutable {
        ++count;
        
        if (limit && count > *limit) {
            std::cout << "레이트 리밋 초과: " << count << "/" << *limit << "\n";
            return false;
        }
        
        std::cout << "요청 허용: " << count;
        if (limit) {
            std::cout << "/" << *limit;
        }
        std::cout << "\n";
        return true;
    };
}

// 사용 예시
void example_http_handlers() {
    // 관리자 권한 필요 (레벨 10)
    auto admin_handler = make_auth_handler(10);
    
    // 일반 사용자 권한 (레벨 체크 없음)
    auto user_handler = make_auth_handler(std::nullopt);
    
    // 레이트 리미터 (100 요청)
    auto limiter = make_rate_limiter(100);
    
    // 요청 처리
    RequestContext ctx;
    ctx.headers["X-User-ID"] = "42";
    
    if (limiter(ctx) && user_handler(ctx)) {
        std::cout << "요청 처리 중...\n";
    }
}

예제 3: 로깅 시스템 with 지연 초기화

#include <fstream>
#include <chrono>
#include <iomanip>

class Logger {
public:
    Logger(std::string filename) 
        : file_(std::make_unique<std::ofstream>(filename, std::ios::app)) {
        if (!file_->is_open()) {
            throw std::runtime_error("로그 파일 열기 실패");
        }
    }
    
    void log(const std::string& level, const std::string& message) {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        
        *file_ << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
               << " [" << level << "] " << message << std::endl;
    }

private:
    std::unique_ptr<std::ofstream> file_;
};

// 비동기 로깅 작업 생성
auto make_async_logger(std::string filename) {
    // Logger를 람다 안에서 생성 (지연 초기화)
    return [
        logger = std::make_unique<Logger>(filename)
    ](const std::string& level, const std::string& message) mutable {
        // 스레드 풀에서 실행될 수 있는 람다
        logger->log(level, message);
    };
}

// 배치 로깅 시스템
void example_batch_logging() {
    auto logger = make_async_logger("app.log");
    
    // 여러 로그 메시지를 비동기로 처리
    std::vector<std::thread> workers;
    
    for (int i = 0; i < 5; ++i) {
        workers.emplace_back([
            logger = std::move(logger),
            worker_id = i
        ]() mutable {
            for (int j = 0; j < 10; ++j) {
                logger("INFO", 
                    "Worker " + std::to_string(worker_id) + 
                    " - 작업 " + std::to_string(j));
                
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        });
        
        // logger를 이동했으므로 다음 반복에서는 새로 생성 필요
        logger = make_async_logger("app.log");
    }
    
    for (auto& t : workers) {
        t.join();
    }
}

예제 4: 타이머 콜백 with 상태 캡처

#include <chrono>
#include <functional>

class Timer {
public:
    template<typename Callback>
    void schedule(std::chrono::milliseconds delay, Callback&& callback) {
        std::thread([
            cb = std::forward<Callback>(callback),
            delay
        ]() {
            std::this_thread::sleep_for(delay);
            cb();
        }).detach();
    }
};

// 리소스를 가진 타이머 콜백
void example_timer_with_resources() {
    Timer timer;
    
    // 예제 1: 카운터를 가진 콜백
    auto counter_callback = [count = 0]() mutable {
        ++count;
        std::cout << "타이머 실행 " << count << "번째\n";
    };
    
    timer.schedule(std::chrono::milliseconds(100), counter_callback);
    
    // 예제 2: 리소스를 가진 콜백
    auto resource = std::make_unique<std::vector<int>>(1000);
    
    timer.schedule(
        std::chrono::milliseconds(200),
        [data = std::move(resource)]() {
            std::cout << "데이터 크기: " << data->size() << "\n";
            // 콜백 종료 시 data 자동 삭제
        }
    );
    
    // 예제 3: 여러 상태를 캡처
    std::string message = "Hello";
    int retry_count = 0;
    auto config = std::make_unique<Config>();
    
    timer.schedule(
        std::chrono::seconds(1),
        [
            msg = std::move(message),      // 문자열 이동
            retry = retry_count,            // 정수 복사
            cfg = std::move(config)        // unique_ptr 이동
        ]() {
            std::cout << "메시지: " << msg << "\n";
            std::cout << "재시도: " << retry << "\n";
            cfg->apply();
        }
    );
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

예제 5: C++11 vs C++14 마이그레이션

// C++11 스타일: 임시 변수 필요
void cpp11_style() {
    std::vector<int> v = load_large_data();
    
    // 방법 1: 임시 변수로 이동
    std::vector<int> v_moved = std::move(v);
    auto f = [v_moved]() { 
        return v_moved.size(); 
    };
    
    // 방법 2: shared_ptr로 감싸기 (오버헤드)
    auto v_shared = std::make_shared<std::vector<int>>(load_large_data());
    auto g = [v_shared]() { 
        return v_shared->size(); 
    };
}

// C++14 스타일: 초기화 캡처로 간결하게
void cpp14_style() {
    std::vector<int> v = load_large_data();
    
    // 한 줄로 해결
    auto f = [vec = std::move(v)]() { 
        return vec.size(); 
    };
    
    // 복잡한 표현식도 가능
    auto g = [
        data = preprocess(load_large_data()),
        config = std::make_unique<Config>(),
        timestamp = std::chrono::system_clock::now()
    ]() {
        return process(data, *config, timestamp);
    };
}

// 실무 마이그레이션 예제
class LegacyService {
public:
    // C++11: 복잡한 헬퍼 함수 필요
    std::function<void()> create_task_cpp11(std::vector<int> data) {
        auto shared_data = std::make_shared<std::vector<int>>(std::move(data));
        return [shared_data]() {
            process(*shared_data);
        };
    }
    
    // C++14: 간결한 구현
    auto create_task_cpp14(std::vector<int> data) {
        return [data = std::move(data)]() {
            process(data);
        };
    }
};

예제 6: 에러 핸들링 패턴

#include <system_error>
#include <expected>  // C++23

// 에러를 캡처하는 콜백
auto make_error_handler(std::string context) {
    return [
        ctx = std::move(context),
        error_count = 0
    ](const std::exception& e) mutable {
        ++error_count;
        std::cerr << "[" << ctx << "] 에러 #" << error_count 
                  << ": " << e.what() << std::endl;
        
        if (error_count > 3) {
            throw std::runtime_error("에러 임계값 초과");
        }
    };
}

// 재시도 로직
auto make_retry_handler(int max_retries) {
    return [
        max = max_retries,
        current = 0,
        backoff = std::chrono::milliseconds(100)
    ]() mutable -> bool {
        if (current >= max) {
            return false;
        }
        
        ++current;
        std::cout << "재시도 " << current << "/" << max << "\n";
        
        // 지수 백오프
        std::this_thread::sleep_for(backoff);
        backoff *= 2;
        
        return true;
    };
}

// 사용 예시
void example_error_handling() {
    auto error_handler = make_error_handler("DB작업");
    auto retry_handler = make_retry_handler(3);
    
    while (retry_handler()) {
        try {
            // 실패할 수 있는 작업
            risky_operation();
            break;  // 성공
        } catch (const std::exception& e) {
            error_handler(e);
        }
    }
}

[*this] / [=, *this] (C++17)

*this 캡처는 클래스 멤버 함수 안의 람다에서 현재 객체의 복사본을 저장할 때 씁니다. “참조로 this만 잡아 두었다가 객체 수명이 끝나는” 실수를 줄입니다.

struct S {
    int n = 0;
    auto make_lambda() {
        return [*this]() { return n; };  // S의 복사본이 클로저에 저장
    }
};

초기화 캡처([n = x])와 목적이 비슷하게 값 스냅샷을 만든다는 점에서 같이 이해하면 좋습니다.

흔한 실수와 디버깅 가이드

실수 1: 이동 후 외부 변수 재사용

문제 코드:

void dangerous_use_after_move() {
    auto ptr = std::make_unique<int>(42);
    
    // ptr을 람다로 이동
    auto task = [p = std::move(ptr)]() {
        return *p;
    };
    
    // 위험! ptr은 이미 비어있음
    if (*ptr > 0) {  // 🚨 정의되지 않은 동작!
        std::cout << "값: " << *ptr << std::endl;
    }
    
    // 더 위험한 패턴
    ptr.reset(new int(100));  // 크래시 또는 이상 동작
}

증상:

  • Segmentation fault (Linux/Mac)
  • Access violation (Windows)
  • AddressSanitizer: heap-use-after-free
  • 디버거에서 0x0 주소 접근 확인

해결책:

void safe_move_pattern() {
    auto ptr = std::make_unique<int>(42);
    
    auto task = [p = std::move(ptr)]() {
        return *p;
    };
    
    // 패턴 1: nullptr 체크
    if (ptr != nullptr) {
        // 이 블록은 실행되지 않음
    }
    
    // 패턴 2: 이동 후 명시적으로 nullptr 할당 (중복이지만 명확)
    ptr = nullptr;
    assert(ptr == nullptr);  // 문서화 목적
    
    // 패턴 3: 다른 이름 사용으로 혼란 방지
    // [new_name = std::move(ptr)] 형태로 이동
}

// 디버그 빌드에서 확인
void debug_check() {
    auto ptr = std::make_unique<int>(42);
    auto task = [p = std::move(ptr)]() { return *p; };
    
    #ifdef _DEBUG
        assert(ptr == nullptr && "ptr should be null after move");
    #endif
}

컴파일러 경고 활성화:

# Clang
clang++ -Wconsumed -Wunused-value file.cpp

# GCC
g++ -Wuse-after-move file.cpp

# MSVC
cl /W4 /analyze file.cpp

실수 2: 참조 캡처와 초기화 캡처 혼동

문제 이해:

void reference_vs_init_capture() {
    int counter = 0;
    
    // 패턴 1: 참조 캡처 - 외부 변수를 계속 추적
    auto ref_lambda = [&counter]() {
        ++counter;  // 외부 counter 수정
    };
    
    ref_lambda();
    std::cout << counter << std::endl;  // 1 (외부 변수 변경됨)
    
    // 패턴 2: 초기화 캡처 - 정의 시점에 스냅샷
    int value = 10;
    auto init_lambda = [val = value]() mutable {
        ++val;  // 람다 내부 멤버 수정
        return val;
    };
    
    init_lambda();  // 11 반환
    std::cout << value << std::endl;  // 10 (외부 변수 그대로)
}

실무 함정:

// 함정 1: 의도와 다른 동작
void counter_trap() {
    int total = 0;
    std::vector<std::function<void()>> tasks;
    
    // 잘못된 의도: 각 태스크가 total을 증가시키길 원함
    for (int i = 0; i < 5; ++i) {
        // ❌ 잘못됨: 각 람다가 독립적인 복사본을 가짐
        tasks.push_back([total = total]() mutable {
            ++total;  // 각자의 total 증가
            std::cout << total << " ";
        });
    }
    
    for (auto& task : tasks) {
        task();  // 출력: 1 1 1 1 1 (기대: 1 2 3 4 5)
    }
    std::cout << "\n최종 total: " << total << std::endl;  // 0
    
    // ✅ 올바름: 참조 캡처 또는 shared 상태
    total = 0;
    tasks.clear();
    
    auto shared_total = std::make_shared<int>(0);
    for (int i = 0; i < 5; ++i) {
        tasks.push_back([total = shared_total]() {
            ++(*total);
            std::cout << *total << " ";
        });
    }
    
    for (auto& task : tasks) {
        task();  // 출력: 1 2 3 4 5
    }
}

// 함정 2: 일시적 값의 스냅샷
void snapshot_confusion() {
    std::vector<int> vec = {1, 2, 3};
    
    // ❌ 잘못된 이해: vec을 추적한다고 생각
    auto lambda = [size = vec.size()]() {
        return size;  // 3 (정의 시점의 크기)
    };
    
    vec.push_back(4);
    vec.push_back(5);
    
    std::cout << lambda() << std::endl;  // 여전히 3 (5가 아님!)
    
    // ✅ 추적이 필요하면 참조 캡처
    auto ref_lambda = [&vec]() {
        return vec.size();
    };
    
    std::cout << ref_lambda() << std::endl;  // 5
}

실수 3: 기본 캡처와 초기화 캡처 혼용 오류

잘못된 코드:

void mixed_capture_errors() {
    int x = 10;
    int y = 20;
    
    // ❌ 에러: 같은 이름을 두 번 캡처
    // auto bad1 = [=, x = x * 2]() { };  // 컴파일 에러!
    
    // ❌ 헷갈리는 코드: [=]가 y도 캡처하는데 명시적으로 또 캡처
    auto bad2 = [=, z = y]() {
        // x는 [=]로 캡처 (값 10)
        // y는 [=]로 캡처 (값 20)
        // z는 초기화 캡처 (값 20)
        // y와 z가 같은 값을 가짐 - 혼란스러움!
    };
}

올바른 패턴:

void correct_mixed_capture() {
    int x = 10;
    int y = 20;
    std::unique_ptr<int> ptr = std::make_unique<int>(30);
    
    // ✅ 명확한 패턴 1: 명시적 캡처만
    auto lambda1 = [
        x,                      // 값 복사
        y_doubled = y * 2,     // 변환하여 캡처
        p = std::move(ptr)     // 이동 캡처
    ]() {
        // 명확: 무엇을 캡처했는지 한눈에 파악
    };
    
    // ✅ 명확한 패턴 2: 기본 캡처 + 예외적인 항목만
    ptr = std::make_unique<int>(40);
    auto lambda2 = [
        &,                     // 나머지는 모두 참조
        p = std::move(ptr)     // 이것만 이동 캡처
    ]() {
        // &로 x, y 참조
        // p는 소유권 이동
    };
}

// 코드 리뷰에서 선호되는 스타일
void review_friendly_style() {
    // ❌ 피해야 할 패턴
    auto bad = [=]() {  // 무엇을 캡처했는지 불명확
        // ...
    };
    
    // ✅ 권장 패턴
    int x = 10, y = 20, z = 30;
    auto good = [x, y, z]() {  // 명시적으로 나열
        // 무엇을 캡처했는지 명확
    };
}

실수 4: 참조 수명 문제 (Dangling Reference)

위험한 패턴:

// 🚨 매우 위험한 코드
auto make_dangerous_lambda() {
    std::vector<int> local_data = {1, 2, 3, 4, 5};
    
    // ❌ local_data의 참조를 반환
    return [&local_data]() {
        return local_data.size();
    };
}  // local_data 파괴 → 람다는 댕글링 참조!

void use_dangerous_lambda() {
    auto lambda = make_dangerous_lambda();
    // lambda()  // 🚨 정의되지 않은 동작! (크래시, 쓰레기 값 등)
}

// 초기화 캡처도 참조를 저장할 수 있음 (위험)
auto make_dangerous_init_capture() {
    int local = 42;
    
    // ❌ 참조를 초기화 캡처 (여전히 위험)
    return [&ref = local]() {
        return ref;
    };
}  // local 파괴 → 댕글링

안전한 해결책:

// ✅ 해결 1: 값으로 소유권 이전
auto make_safe_lambda_value() {
    std::vector<int> local_data = {1, 2, 3, 4, 5};
    
    return [data = std::move(local_data)]() {
        return data.size();  // 안전: 람다가 소유권을 가짐
    };
}

// ✅ 해결 2: shared_ptr로 수명 공유
auto make_safe_lambda_shared() {
    auto data = std::make_shared<std::vector<int>>(
        std::vector<int>{1, 2, 3, 4, 5}
    );
    
    return [data]() {
        return data->size();  // 안전: 참조 카운터로 관리
    };
}

// ✅ 해결 3: 정적 데이터 참조 (수명 보장)
auto make_safe_lambda_static() {
    static const std::vector<int> static_data = {1, 2, 3, 4, 5};
    
    return [&static_data]() {
        return static_data.size();  // 안전: 정적 수명
    };
}

디버깅 도구:

# AddressSanitizer로 수명 문제 감지
g++ -fsanitize=address -g file.cpp -o program
./program

# Valgrind로 메모리 문제 확인
valgrind --leak-check=full ./program

# 출력 예:
# ==12345== Invalid read of size 8
# ==12345==    at 0x...: lambda::operator()
# ==12345==  Address 0x... is 24 bytes inside a block of size 32 free'd

실수 5: mutable 키워드 누락

문제 코드:

void missing_mutable() {
    // ❌ 에러: const 람다에서 멤버 수정 시도
    auto counter = [n = 0]() {
        // return ++n;  // 컴파일 에러!
        // error: cannot assign to a variable captured by copy in a non-mutable lambda
    };
    
    // ✅ mutable 추가
    auto counter_ok = [n = 0]() mutable {
        return ++n;  // OK
    };
    
    std::cout << counter_ok() << std::endl;  // 1
    std::cout << counter_ok() << std::endl;  // 2
    std::cout << counter_ok() << std::endl;  // 3
}

const vs mutable 이해:

// 내부 동작 이해
class __lambda_const {
    int n;
public:
    __lambda_const(int init) : n(init) {}
    
    // operator()가 const
    auto operator()() const {
        // return ++n;  // 에러: const 함수에서 멤버 수정 불가
    }
};

class __lambda_mutable {
    int n;
public:
    __lambda_mutable(int init) : n(init) {}
    
    // operator()가 non-const
    auto operator()() {  // const 없음!
        return ++n;  // OK
    }
};

스레드 안전성 주의:

void thread_safety_issue() {
    auto counter = [n = 0]() mutable {
        return ++n;
    };
    
    // ❌ 스레드 안전하지 않음!
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i++) {
        threads.emplace_back([&counter]() {
            std::cout << counter() << " ";  // 레이스 컨디션!
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    // 출력: 예측 불가능 (1 2 3 4... 순서 보장 안 됨, 값 중복 가능)
    
    // ✅ 스레드 안전한 버전
    std::atomic<int> atomic_counter{0};
    auto safe_counter = [&atomic_counter]() {
        return ++atomic_counter;
    };
    
    threads.clear();
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([safe_counter]() {
            std::cout << safe_counter() << " ";
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
}

실수 6: 복사 비용 간과

void expensive_copy_mistake() {
    std::vector<int> large_data(1'000'000);
    
    // ❌ 의도치 않은 복사
    auto bad = [data = large_data]() {  // 복사 발생!
        return data.size();
    };
    
    // ✅ 의도한 이동
    auto good = [data = std::move(large_data)]() {
        return data.size();
    };
    
    // 문자열도 마찬가지
    std::string long_str(10'000, 'a');
    
    auto bad_str = [s = long_str]() {  // 복사
        return s.length();
    };
    
    auto good_str = [s = std::move(long_str)]() {  // 이동
        return s.length();
    };
}

디버깅 체크리스트

// 초기화 캡처 사용 시 확인할 사항
void debugging_checklist() {
    // ✓ 1. 이동 후 원본 변수 사용하지 않았는지?
    auto ptr = std::make_unique<int>(42);
    auto task = [p = std::move(ptr)]() {};
    // assert(ptr == nullptr);
    
    // ✓ 2. 수명 문제 없는지? (참조는 위험)
    // 로컬 변수를 참조로 캡처하여 반환하지 않았는지?
    
    // ✓ 3. mutable 필요한지?
    auto counter = [n = 0]() mutable { return ++n; };
    
    // ✓ 4. 복사 vs 이동 의도가 명확한지?
    std::vector<int> v;
    auto copy_lambda = [data = v]() {};        // 복사
    auto move_lambda = [data = std::move(v)]() {};  // 이동
    
    // ✓ 5. 스레드 안전성 고려했는지?
    // 여러 스레드에서 같은 람다 호출 시 동기화 필요
    
    // ✓ 6. 람다 자체가 이동 가능한지 확인
    // unique_ptr 캡처 시 람다는 move-only
}

성능 고려사항

복사 vs 이동 벤치마크

#include <chrono>
#include <vector>
#include <iostream>

void performance_comparison() {
    const int SIZE = 10'000'000;
    std::vector<int> data(SIZE, 42);
    
    // 복사 캡처 측정
    auto start = std::chrono::high_resolution_clock::now();
    auto copy_lambda = [data]() { return data.size(); };
    copy_lambda();
    auto end = std::chrono::high_resolution_clock::now();
    auto copy_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    // 이동 캡처 측정
    data = std::vector<int>(SIZE, 42);  // 재생성
    start = std::chrono::high_resolution_clock::now();
    auto move_lambda = [data = std::move(data)]() { return data.size(); };
    move_lambda();
    end = std::chrono::high_resolution_clock::now();
    auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    std::cout << "복사 캡처: " << copy_time.count() << " μs\n";
    std::cout << "이동 캡처: " << move_time.count() << " μs\n";
    std::cout << "속도 향상: " << (copy_time.count() / (double)move_time.count()) << "배\n";
    
    // 예상 출력:
    // 복사 캡처: ~40000 μs (40ms)
    // 이동 캡처: ~2 μs
    // 속도 향상: ~20000배
}

메모리 사용량

void memory_usage() {
    // unique_ptr: 포인터 크기만 (8 bytes on 64-bit)
    auto unique_lambda = [p = std::make_unique<int>(42)]() {};
    std::cout << "unique_ptr 람다: " << sizeof(unique_lambda) << " bytes\n";
    
    // shared_ptr: 포인터 2개 (16 bytes: 객체 + 제어블록)
    auto shared_lambda = [p = std::make_shared<int>(42)]() {};
    std::cout << "shared_ptr 람다: " << sizeof(shared_lambda) << " bytes\n";
    
    // 값 캡처: 실제 데이터 크기
    std::vector<int> vec(1000);
    auto value_lambda = [vec]() {};
    std::cout << "vector 값 캡처: " << sizeof(value_lambda) << " bytes\n";
    // ~24 bytes (vector 내부 포인터 3개)
    
    // 참조 캡처: 포인터 크기 (8 bytes)
    auto ref_lambda = [&vec]() {};
    std::cout << "vector 참조 캡처: " << sizeof(ref_lambda) << " bytes\n";
}

모범 사례 및 가이드라인

1. 기본 원칙

// ✅ DO: 명시적이고 최소한의 캡처
auto good1 = [x, y]() { return x + y; };

// ❌ DON'T: 불명확한 기본 캡처
auto bad1 = [=]() { /* 무엇을 캡처했는지 불명확 */ };

// ✅ DO: 이동 의미론 명확하게
auto good2 = [ptr = std::move(ptr)]() { ptr->use(); };

// ❌ DON'T: 복사/이동 의도가 불명확
auto bad2 = [ptr]() { /* 복사? 컴파일 에러? */ };

2. 네이밍 컨벤션

// 패턴 1: 접미사 사용
auto ptr = std::make_unique<Resource>();
auto lambda1 = [ptr_ = std::move(ptr)]() {
    ptr_->use();
};

// 패턴 2: 새 의미 있는 이름
auto connection = std::make_unique<Connection>();
auto lambda2 = [conn = std::move(connection)]() {
    conn->query();
};

// 패턴 3: 변환 의도 표현
int celsius = 25;
auto lambda3 = [fahrenheit = celsius * 9/5 + 32]() {
    return fahrenheit;
};

3. 타입 선택 가이드

// 독점 소유권 → unique_ptr
auto make_worker(std::unique_ptr<Task> task) {
    return [task = std::move(task)]() { task->execute(); };
}

// 공유 소유권 → shared_ptr
auto make_multiple_workers(std::shared_ptr<Cache> cache) {
    std::vector<std::function<void()>> workers;
    for (int i = 0; i < 4; ++i) {
        workers.push_back([cache, i]() {
            cache->process(i);  // 모두 같은 캐시 공유
        });
    }
    return workers;
}

// 대용량 데이터 → move
auto make_processor(std::vector<int> data) {
    return [data = std::move(data)]() {
        process_large_data(data);
    };
}

4. 에러 처리 패턴

// 예외 안전성 보장
auto make_safe_task(std::unique_ptr<Resource> res) {
    return [res = std::move(res)]() {
        try {
            res->use();
        } catch (const std::exception& e) {
            // 리소스는 자동으로 정리됨
            log_error(e.what());
            throw;
        }
    };  // res가 자동 삭제됨 (RAII)
}

// 옵셔널 리소스
auto make_optional_task(std::optional<Config> config) {
    return [cfg = std::move(config)]() {
        if (cfg) {
            use_config(*cfg);
        } else {
            use_default_config();
        }
    };
}

5. 문서화 및 주석

// 의도를 명확히 하는 주석
class TaskManager {
    // 리소스 소유권을 태스크로 이전
    void schedule_task(std::unique_ptr<Data> data) {
        tasks_.push_back([
            data = std::move(data)  // 소유권 이전 (원본은 nullptr)
        ]() {
            data->process();
        });
    }
    
    // 데이터 스냅샷 캡처 (원본은 유지)
    void schedule_snapshot(const Config& config) {
        tasks_.push_back([
            cfg = config  // 복사본 생성
        ]() {
            use_config(cfg);
        });
    }

private:
    std::vector<std::function<void()>> tasks_;
};

언어 버전별 지원

기능C++11C++14C++17C++20
기본 캡처 [x], [&x]
초기화 캡처 [x = expr]
[*this] 캡처
[=, this] 경고
템플릿 람다
constexpr 람다
[...]<typename T>

요약 및 빠른 참조

핵심 요약

주제C++11C++14 개선
기본 캡처[x], [&x], [=], [&]동일
초기화 캡처❌ 불가능[name = expr]
Move 캡처임시 변수 필요[name = std::move(x)]
변환 캡처임시 변수 필요[doubled = x * 2]
use case단순 복사/참조이동 전용 타입, 대용량 데이터

문법 빠른 참조

// 값 복사
[x]()                  // x 복사

// 참조
[&x]()                 // x 참조

// 초기화 (C++14+)
[x = expr]()           // expr 평가 결과로 x 초기화
[x = std::move(y)]()   // y를 x로 이동
[x = y * 2]()          // 변환하여 캡처

// 혼합
[x, &y, z = std::move(w)]()  // x 복사, y 참조, w 이동

// mutable
[x = 0]() mutable { ++x; }   // 캡처 변수 수정 가능

실무 의사결정 플로우

캡처가 필요한가?

├─ 복사 가능? 
│  ├─ 예 → 작은 객체? → [x] (값 캡처)
│  └─ 아니오 → 다음으로

├─ 이동 가능?
│  ├─ 예 → [x = std::move(y)] (이동 캡처)
│  └─ 아니오 → 다음으로

├─ 공유 필요?
│  ├─ 예 → shared_ptr로 감싸서 [ptr] (복사)
│  └─ 아니오 → 다음으로

└─ 참조만 필요?
   ├─ 수명 보장? → [&x] (참조)
   └─ 수명 불명확? → 재설계 필요

흔한 실수 요약

실수증상해결
이동 후 사용Segfault, nullptr 역참조nullptr 체크, 다른 이름 사용
참조 수명 문제Dangling reference, 이상 동작값 캡처, shared_ptr 사용
mutable 누락컴파일 에러mutable 키워드 추가
의도치 않은 복사성능 저하std::move 명시
스레드 안전성레이스 컨디션std::atomic, mutex 사용

관련 글: 람다 캡처 상세, make_unique, 커스텀 삭제자.

같이 보면 좋은 글 (내부 링크)


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 초기화 캡처 | C++14 init-capture, move·unique_ptr 패턴 완전 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 초기화 캡처 | C++14 init-capture, move·unique_ptr 패턴 완전 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++14 init-capture 심화: 이동 의미론, 클로저 멤버 생성, 완벽 전달(forward), 캡처 메모리 레이아웃, 프로덕션 람다 패턴. unique_ptr·실무 함정까지. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, lambda, init-capture, C++14, move-semantics, unique_ptr, ABI, forwarding 등으로 검색하시면 이 글이 도움이 됩니다.