C++ 람다 & 클로저 완전 가이드
개요 — 람다란
Section titled “개요 — 람다란”람다(Lambda)는 **이름 없는 익명 함수 객체(Closure)**를 코드 중간에 정의하는 문법입니다. C++11에서 도입되어 STL 알고리즘, 콜백, 비동기 처리에서 코드를 크게 간결하게 만듭니다.
// 함수 포인터 방식 (전통)bool IsPositive(int N) { return N > 0; }std::sort(Vec.begin(), Vec.end(), IsPositive); // 별도 함수 정의 필요
// 람다 방식std::sort(Vec.begin(), Vec.end(), [](int A, int B) { return A < B; });1. 람다 문법 구조
Section titled “1. 람다 문법 구조”[캡처](파라미터) mutable -> 반환타입 { 본문 } ① ② ③ ④ ⑤// 가장 단순한 람다auto Hello = []() { std::cout << "Hello!\n"; };Hello(); // 호출
// 파라미터 있는 람다auto Add = [](int A, int B) -> int { return A + B; };int Result = Add(3, 4); // 7
// 반환 타입 추론 (C++14 이후 거의 모든 경우 생략 가능)auto Multiply = [](int A, int B) { return A * B; };2. 캡처 (Capture)
Section titled “2. 캡처 (Capture)”람다 바깥 스코프의 변수를 람다 내부에서 사용하려면 캡처해야 합니다.
2.1 값 캡처 [=] / [변수명]
Section titled “2.1 값 캡처 [=] / [변수명]”int Base = 10;
// 특정 변수 값 캡처auto AddBase = [Base](int N) { return N + Base; };Base = 999; // 변경해도 람다 내부 Base는 10 그대로int R = AddBase(5); // 15
// 전체 값 캡처auto Lambda = [=](int N) { return N + Base; }; // 모든 지역변수 복사2.2 참조 캡처 [&] / [&변수명]
Section titled “2.2 참조 캡처 [&] / [&변수명]”int Counter = 0;
// 참조 캡처 — 외부 변수를 직접 수정 가능auto Increment = [&Counter]() { ++Counter; };Increment();Increment();std::cout << Counter; // 2
// 전체 참조 캡처 (주의: 람다 수명이 변수 수명을 초과하면 위험)auto Lambda = [&]() { ++Counter; };2.3 mutable — 값 캡처 변수 수정
Section titled “2.3 mutable — 값 캡처 변수 수정”값 캡처된 변수는 기본적으로 const입니다. mutable을 붙이면 람다 내부 복사본을 수정할 수 있습니다.
int Score = 0;
// mutable 없으면 컴파일 오류auto BadLambda = [Score]() { Score += 10; }; // 오류!
// mutable로 내부 복사본 수정 가능 (외부 Score는 변경 안 됨)auto GoodLambda = [Score]() mutable{ Score += 10; // 복사본 수정 std::cout << Score; // 10};GoodLambda();std::cout << Score; // 0 — 외부는 그대로2.4 초기화 캡처 (C++14) — 이동 캡처
Section titled “2.4 초기화 캡처 (C++14) — 이동 캡처”// 이동 전용 타입(unique_ptr) 캡처auto UPtr = std::make_unique<int>(42);
// [=]로는 unique_ptr 캡처 불가 (복사 불가)// 초기화 캡처로 이동auto Lambda = [Ptr = std::move(UPtr)](){ std::cout << *Ptr;};
// UPtr는 이제 nullptr (소유권이 Lambda로 이전됨)
// 표현식으로 초기화int X = 10;auto Lambda2 = [Y = X * 2]() { std::cout << Y; }; // Y = 20으로 초기화2.5 this 캡처
Section titled “2.5 this 캡처”class MyClass{ int Value = 42;
void SetupCallbacks() { // [this] — this 포인터 값 캡처 (위험: 객체 소멸 후 댕글링) auto L1 = [this]() { return Value; };
// [*this] (C++17) — 객체 전체를 복사 캡처 (안전하지만 비용 발생) auto L2 = [*this]() { return Value; };
// 실전: shared_from_this로 안전하게 캡처 // auto L3 = [Weak = weak_from_this()]() { // if (auto Shared = Weak.lock()) { Shared->DoSomething(); } // }; }};2.6 캡처 방식 요약
Section titled “2.6 캡처 방식 요약”| 캡처 | 의미 |
|---|---|
[] | 캡처 없음 |
[=] | 모든 지역변수 값 복사 |
[&] | 모든 지역변수 참조 |
[x] | x만 값 복사 |
[&x] | x만 참조 |
[=, &x] | 기본 값 복사, x만 참조 |
[x = expr] | 초기화 캡처 (C++14) |
[this] | this 포인터 캡처 |
[*this] | 객체 전체 복사 캡처 (C++17) |
3. 제네릭 람다 (C++14)
Section titled “3. 제네릭 람다 (C++14)”파라미터 타입에 auto를 사용해 템플릿처럼 동작합니다.
// auto 파라미터 — 컴파일러가 타입 추론auto Print = [](auto Value){ std::cout << Value << "\n";};
Print(42); // intPrint(3.14); // doublePrint("hello"); // const char*
// 복수 auto 파라미터auto Add = [](auto A, auto B) { return A + B; };auto R1 = Add(1, 2); // intauto R2 = Add(1.5, 2.5); // doubleauto R3 = Add(std::string("Hello"), std::string(" World")); // string// 제네릭 람다로 정렬std::vector<std::pair<int, std::string>> Data = {{3, "C"}, {1, "A"}, {2, "B"}};
// 첫 번째 요소 기준 정렬std::sort(Data.begin(), Data.end(), [](const auto& A, const auto& B){ return A.first < B.first;});4. 즉시 호출 람다 (IIFE)
Section titled “4. 즉시 호출 람다 (IIFE)”정의와 동시에 호출하는 패턴입니다. 복잡한 초기화 로직을 const 변수에 담을 때 유용합니다.
// const 변수에 복잡한 초기화const int ProcessedValue = [&](){ int Raw = GetRawData(); if (Raw < 0) return 0; if (Raw > 100) return 100; return Raw * 2;}(); // 끝에 () — 즉시 호출
// switch로 초기화하기 어려운 경우const std::string StatusText = [Status](){ switch (Status) { case 0: return "Idle"; case 1: return "Running"; case 2: return "Stopped"; default: return "Unknown"; }}();5. std::function — 타입 소거 래퍼
Section titled “5. std::function — 타입 소거 래퍼”std::function<반환타입(파라미터...)>는 람다, 함수 포인터, 함수 객체를 통일된 타입으로 저장합니다.
#include <functional>
// 어떤 callable이든 저장 가능std::function<int(int, int)> Op;
Op = [](int A, int B) { return A + B; }; // 람다int Sum = Op(3, 4); // 7
Op = std::plus<int>{}; // 함수 객체int Sum2 = Op(3, 4); // 7
// 멤버 함수struct Adder { int Add(int A, int B) { return A + B; } };Adder Obj;Op = std::bind(&Adder::Add, &Obj, std::placeholders::_1, std::placeholders::_2);std::function vs 람다 직접 사용 — 성능 비교
Section titled “std::function vs 람다 직접 사용 — 성능 비교”// 직접 auto — 컴파일러가 인라인 가능, 가장 빠름auto DirectLambda = [](int N) { return N * 2; };
// std::function — 타입 소거로 가상 디스패치 발생, 힙 할당 가능std::function<int(int)> WrappedLambda = [](int N) { return N * 2; };
// 결론:// - 타입을 저장·전달할 필요가 있을 때만 std::function 사용// - 템플릿 파라미터로 전달하면 auto가 훨씬 빠름// 권장: 템플릿으로 받으면 인라인 최적화 가능template<typename Func>void ForEach(std::vector<int>& Vec, Func&& Callback){ for (int& V : Vec) { Callback(V); }}
// 비권장: std::function은 오버헤드 있음void ForEachSlow(std::vector<int>& Vec, std::function<void(int&)> Callback){ for (int& V : Vec) { Callback(V); }}6. 재귀 람다
Section titled “6. 재귀 람다”람다는 자기 자신을 이름으로 참조할 수 없어 재귀가 까다롭습니다.
// std::function 사용 (오버헤드 있음)std::function<int(int)> Factorial = [&Factorial](int N) -> int{ return N <= 1 ? 1 : N * Factorial(N - 1);};
// C++23: 명시적 this 파라미터 (deducing this)// auto Factorial = [](this auto&& Self, int N) -> int// {// return N <= 1 ? 1 : N * Self(N - 1);// };7. 실전 패턴
Section titled “7. 실전 패턴”7.1 이벤트 핸들러 등록
Section titled “7.1 이벤트 핸들러 등록”class Button{public: void SetOnClick(std::function<void()> Handler) { OnClick = std::move(Handler); } void Click() { if (OnClick) OnClick(); }
private: std::function<void()> OnClick;};
Button Btn;int ClickCount = 0;
// 람다로 상태 캡처Btn.SetOnClick([&ClickCount](){ ++ClickCount; std::cout << "클릭 횟수: " << ClickCount;});
Btn.Click(); // 클릭 횟수: 17.2 지연 실행 (Deferred Execution)
Section titled “7.2 지연 실행 (Deferred Execution)”// 작업 큐에 람다 저장 후 나중에 실행std::vector<std::function<void()>> TaskQueue;
void ScheduleTask(std::function<void()> Task){ TaskQueue.push_back(std::move(Task));}
void FlushTasks(){ for (auto& Task : TaskQueue) { Task(); } TaskQueue.clear();}
// 등록int PlayerHP = 100;ScheduleTask([&PlayerHP]() { PlayerHP -= 10; });ScheduleTask([]() { std::cout << "데미지 처리 완료\n"; });
FlushTasks(); // 나중에 일괄 실행7.3 RAII 스코프 가드
Section titled “7.3 RAII 스코프 가드”// 람다로 스코프 종료 시 실행할 작업 등록class ScopeGuard{public: explicit ScopeGuard(std::function<void()> Cleanup) : CleanupFn(std::move(Cleanup)) {}
~ScopeGuard() { if (CleanupFn) CleanupFn(); }
ScopeGuard(const ScopeGuard&) = delete; ScopeGuard& operator=(const ScopeGuard&) = delete;
private: std::function<void()> CleanupFn;};
void RiskyOperation(){ OpenFile("data.bin"); ScopeGuard Guard([]() { CloseFile(); }); // 스코프 종료 시 자동 실행
// ... 예외가 발생해도 CloseFile()은 반드시 호출됨}| 상황 | 권장 |
|---|---|
| 일회성 콜백·정렬 비교자 | auto + 람다 직접 |
| 타입을 저장·전달해야 할 때 | std::function<> |
| 외부 변수 수정 필요 | 참조 캡처 [&var] |
| 람다 수명 > 변수 수명 | 값 캡처 [var] 또는 초기화 캡처 |
| 이동 전용 타입 캡처 | 초기화 캡처 [ptr = std::move(ptr)] |
| 복잡한 const 초기화 | IIFE 패턴 |
핵심 규칙:
[&]로 전체 참조 캡처 시 람다 수명을 반드시 확인 — 댕글링 참조 위험std::function은 편리하지만 성능이 중요한 핫패스에서는 템플릿으로 대체this를 캡처하는 람다는 객체 소멸 후 호출되지 않도록 수명을 관리