C++ 람다 캡처 | "Lambda Capture" 완벽 가이드

C++ 람다 캡처 | "Lambda Capture" 완벽 가이드

이 글의 핵심

람다 캡처(Lambda Capture) 는 람다 함수가 외부 변수에 접근하는 방법을 정의합니다. 람다는 자신이 정의된 스코프의 변수를 캡처하여 사용할 수 있으며, 캡처 방식에 따라 값 복사 또는 참조로 접근합니다.

람다 캡처란?

람다 캡처(Lambda Capture) 는 람다 함수가 외부 변수에 접근하는 방법을 정의합니다. 람다는 자신이 정의된 스코프의 변수를 캡처하여 사용할 수 있으며, 캡처 방식에 따라 값 복사 또는 참조로 접근합니다.

int x = 10;

// 값 캡처
auto f1 = [x]() { return x; };

// 참조 캡처
auto f2 = [&x]() { return x; };

// 모든 변수 값 캡처
auto f3 = [=]() { return x; };

// 모든 변수 참조 캡처
auto f4 = [&]() { return x; };

왜 필요한가?:

  • 클로저: 람다가 외부 상태를 “기억”할 수 있음
  • 유연성: 값 또는 참조로 선택적 캡처
  • 간결성: 함수 객체 대신 간단한 문법
  • 타입 안전: 컴파일러가 캡처 검증
// ❌ 함수 객체: 복잡
struct Adder {
    int x;
    Adder(int x) : x(x) {}
    int operator()(int y) const { return x + y; }
};

Adder add10(10);
std::cout << add10(5) << '\n';  // 15

// ✅ 람다 캡처: 간결
int x = 10;
auto add10 = [x](int y) { return x + y; };
std::cout << add10(5) << '\n';  // 15

캡처의 동작 원리:

람다는 내부적으로 익명 함수 객체(Functor) 로 변환됩니다. 캡처된 변수는 함수 객체의 멤버 변수가 됩니다.

int x = 10;
auto f = [x]() { return x; };

// 개념적으로 다음과 같이 변환됨:
struct __lambda {
    int x;  // 캡처된 변수
    __lambda(int x) : x(x) {}
    int operator()() const { return x; }
};

__lambda f(x);

값 캡처 vs 참조 캡처

int x = 10;

// 값 캡처: 복사본
auto f1 = [x]() mutable {
    x++;  // 복사본 수정
    return x;
};

cout << f1() << endl;  // 11
cout << x << endl;     // 10 (원본 변경 안됨)

// 참조 캡처: 원본
auto f2 = [&x]() {
    x++;  // 원본 수정
    return x;
};

cout << f2() << endl;  // 11
cout << x << endl;     // 11 (원본 변경됨)

혼합 캡처

int x = 10;
int y = 20;

// x는 값, y는 참조
auto f = [x, &y]() {
    // x++;  // 에러: 값 캡처는 const
    y++;     // OK: 참조 캡처
    return x + y;
};

cout << f() << endl;  // 31
cout << x << endl;    // 10
cout << y << endl;    // 21

초기화 캡처 (C++14)

// 새 변수 생성
auto f1 = [x = 42]() {
    return x;
};

// 이동 캡처
auto ptr = make_unique<int>(10);
auto f2 = [p = move(ptr)]() {
    return *p;
};

// 표현식 캡처
int x = 10;
auto f3 = [y = x * 2]() {
    return y;
};

cout << f3() << endl;  // 20

실전 예시

예시 1: 카운터

auto makeCounter() {
    int count = 0;
    
    return [count]() mutable {
        return ++count;
    };
}

int main() {
    auto counter = makeCounter();
    
    cout << counter() << endl;  // 1
    cout << counter() << endl;  // 2
    cout << counter() << endl;  // 3
}

예시 2: 필터

vector<int> filterGreaterThan(const vector<int>& vec, int threshold) {
    vector<int> result;
    
    copy_if(vec.begin(), vec.end(), back_inserter(result),
        [threshold](int x) {
            return x > threshold;
        });
    
    return result;
}

int main() {
    vector<int> nums = {1, 5, 3, 8, 2, 9, 4};
    auto filtered = filterGreaterThan(nums, 5);
    
    for (int n : filtered) {
        cout << n << " ";
    }
    cout << endl;  // 8 9
}

예시 3: 이벤트 핸들러

class Button {
private:
    function<void()> onClick;
    
public:
    void setOnClick(function<void()> handler) {
        onClick = handler;
    }
    
    void click() {
        if (onClick) {
            onClick();
        }
    }
};

int main() {
    Button button;
    int clickCount = 0;
    
    // clickCount 참조 캡처
    button.setOnClick([&clickCount]() {
        clickCount++;
        cout << "클릭 " << clickCount << "회" << endl;
    });
    
    button.click();  // 클릭 1회
    button.click();  // 클릭 2회
    button.click();  // 클릭 3회
}

예시 4: 정렬

struct Person {
    string name;
    int age;
};

int main() {
    vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    
    // age로 정렬
    sort(people.begin(), people.end(),
         {
            return a.age < b.age;
        });
    
    for (const auto& p : people) {
        cout << p.name << ": " << p.age << endl;
    }
    // Bob: 25
    // Alice: 30
    // Charlie: 35
}

this 캡처

class Counter {
private:
    int count = 0;
    
public:
    auto getIncrementer() {
        // this 캡처 (멤버 접근)
        return [this]() {
            return ++count;
        };
    }
    
    auto getIncrementerCopy() {
        // this 복사 (C++17)
        return [*this]() mutable {
            return ++count;  // 복사본 수정
        };
    }
    
    int getCount() const {
        return count;
    }
};

int main() {
    Counter counter;
    auto inc = counter.getIncrementer();
    
    cout << inc() << endl;  // 1
    cout << inc() << endl;  // 2
    cout << counter.getCount() << endl;  // 2
}

mutable 키워드

int x = 10;

// 값 캡처는 기본적으로 const
auto f1 = [x]() {
    // x++;  // 에러: const
    return x;
};

// mutable로 수정 가능
auto f2 = [x]() mutable {
    x++;  // OK (복사본 수정)
    return x;
};

cout << f2() << endl;  // 11
cout << x << endl;     // 10 (원본 유지)

자주 발생하는 문제

문제 1: 댕글링 참조

// ❌ 댕글링 참조
function<int()> makeFunc() {
    int x = 10;
    return [&x]() { return x; };  // x는 소멸됨
}

auto f = makeFunc();
// cout << f() << endl;  // UB: x는 이미 소멸

// ✅ 값 캡처
function<int()> makeFunc() {
    int x = 10;
    return [x]() { return x; };  // 복사본
}

문제 2: 캡처 누락

int x = 10;
int y = 20;

// ❌ y 캡처 누락
auto f = [x]() {
    return x + y;  // 에러: y 캡처 안됨
};

// ✅ y 캡처
auto f = [x, y]() {
    return x + y;
};

// 또는 모든 변수 캡처
auto f = [=]() {
    return x + y;
};

문제 3: this 수명

class Widget {
public:
    auto getCallback() {
        // ❌ this 댕글링
        return [this]() {
            // Widget이 소멸되면 UB
        };
    }
    
    // ✅ shared_ptr 사용
    auto getCallback(shared_ptr<Widget> self) {
        return [self]() {
            // 안전
        };
    }
};

캡처 방식 정리

[]        // 캡처 없음
[x]       // x를 값으로 캡처
[&x]      // x를 참조로 캡처
[=]       // 모든 변수 값 캡처
[&]       // 모든 변수 참조 캡처
[=, &x]   // x는 참조, 나머지는 값
[&, x]    // x는 값, 나머지는 참조
[this]    // this 포인터 캡처
[*this]   // this 객체 복사 (C++17)
[x = 42]  // 초기화 캡처 (C++14)

실무 패턴

패턴 1: 지연 실행

class TaskScheduler {
    std::vector<std::function<void()>> tasks_;
    
public:
    void schedule(std::function<void()> task) {
        tasks_.push_back(task);
    }
    
    void executeAll() {
        for (auto& task : tasks_) {
            task();
        }
        tasks_.clear();
    }
};

// 사용
TaskScheduler scheduler;
int x = 10;

scheduler.schedule([x]() {
    std::cout << "작업 1: " << x << '\n';
});

scheduler.schedule([&x]() {
    x++;
    std::cout << "작업 2: " << x << '\n';
});

scheduler.executeAll();

패턴 2: 콜백 체인

class AsyncOperation {
public:
    template<typename F>
    void then(F&& callback) {
        // 비동기 작업 완료 후 콜백 실행
        std::thread([callback = std::forward<F>(callback)]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            callback();
        }).detach();
    }
};

// 사용
AsyncOperation op;
int result = 0;

op.then([&result]() {
    result = 42;
    std::cout << "작업 완료: " << result << '\n';
});

패턴 3: 상태 머신

class StateMachine {
    std::function<void()> currentState_;
    
public:
    void setState(std::function<void()> state) {
        currentState_ = state;
    }
    
    void execute() {
        if (currentState_) {
            currentState_();
        }
    }
};

// 사용
StateMachine sm;
int count = 0;

auto idle = [&]() {
    std::cout << "Idle 상태\n";
    if (count++ > 3) {
        sm.setState([&]() {
            std::cout << "Active 상태\n";
        });
    }
};

sm.setState(idle);
sm.execute();

FAQ

Q1: 값 캡처 vs 참조 캡처?

A:

  • 값 캡처 [x]: 안전 (복사본 사용), 복사 비용 발생, 원본 변경 불가
  • 참조 캡처 [&x]: 빠름 (복사 없음), 댕글링 위험, 원본 변경 가능
int x = 10;

// 값 캡처: 안전하지만 복사 비용
auto f1 = [x]() { return x; };

// 참조 캡처: 빠르지만 수명 주의
auto f2 = [&x]() { return x; };

선택 기준:

  • 람다가 함수 밖으로 반환되면: 값 캡처
  • 람다가 로컬에서만 사용되면: 참조 캡처

Q2: mutable은 언제 사용하나요?

A: 값 캡처한 변수를 수정할 때 사용합니다. 값 캡처는 기본적으로 const이므로 mutable이 필요합니다.

int x = 10;

// ❌ 에러: 값 캡처는 const
auto f1 = [x]() {
    // x++;  // 에러
};

// ✅ mutable: 복사본 수정 가능
auto f2 = [x]() mutable {
    x++;  // OK (복사본 수정)
    return x;
};

std::cout << f2() << '\n';  // 11
std::cout << x << '\n';     // 10 (원본 유지)

Q3: [=] vs [&]?

A:

  • [=]: 모든 변수를 값으로 캡처 (안전, 복사 비용)
  • [&]: 모든 변수를 참조로 캡처 (빠름, 댕글링 위험)
int x = 10, y = 20;

// [=]: 모든 변수 값 캡처
auto f1 = [=]() { return x + y; };

// [&]: 모든 변수 참조 캡처
auto f2 = [&]() { return x + y; };

실무 권장: 명시적 캡처 [x, &y]가 더 명확하고 안전합니다.

Q4: this 캡처는 언제 사용하나요?

A: 멤버 함수에서 멤버 변수나 멤버 함수에 접근할 때 사용합니다.

class Counter {
    int count_ = 0;
    
public:
    auto getIncrementer() {
        // [this]: this 포인터 캡처
        return [this]() {
            return ++count_;
        };
    }
    
    auto getIncrementerCopy() {
        // [*this]: 객체 복사 (C++17)
        return [*this]() mutable {
            return ++count_;  // 복사본 수정
        };
    }
};

Q5: 초기화 캡처는 무엇인가요?

A: C++14에서 도입된 기능으로, 캡처 시 새 변수를 생성하거나 이동 캡처를 수행합니다.

// 새 변수 생성
auto f1 = [x = 42]() { return x; };

// 이동 캡처
auto ptr = std::make_unique<int>(10);
auto f2 = [p = std::move(ptr)]() {
    return *p;
};

// 표현식 캡처
int x = 10;
auto f3 = [y = x * 2]() { return y; };

Q6: 람다 캡처 시 성능 고려사항은?

A:

  • 값 캡처: 복사 비용 (큰 객체는 참조 권장)
  • 참조 캡처: 복사 없음 (빠름)
  • 이동 캡처: 복사 없이 소유권 이전 (C++14)
std::vector<int> vec(1000000);

// ❌ 값 캡처: 큰 복사 비용
auto f1 = [vec]() { return vec.size(); };

// ✅ 참조 캡처: 복사 없음
auto f2 = [&vec]() { return vec.size(); };

// ✅ 이동 캡처: 소유권 이전
auto f3 = [vec = std::move(vec)]() { return vec.size(); };

Q7: 람다 캡처 학습 리소스는?

A:

관련 글: Lambda Complete, Init Capture.

한 줄 요약: 람다 캡처는 외부 변수를 값 또는 참조로 캡처하여 람다 내부에서 사용할 수 있게 합니다.


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

이 주제와 연결되는 다른 글입니다.

  • C++ 람다 함수 | “익명 함수” 완벽 정리 [캡처/mutable]
  • C++ Init Capture | “초기화 캡처” 가이드
  • C++ Generic Lambda | “제네릭 람다” 가이드

관련 글

  • C++ 람다 캡처 에러 |
  • C++ 람다 함수 |
  • C++ std::function vs 함수 포인터 |
  • C++ constexpr Lambda |
  • C++ Generic Lambda |