C++ 람다 & 클로저
개요 — 람다란
섹션 제목: “개요 — 람다란”람다(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. 람다 문법 구조
섹션 제목: “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)
섹션 제목: “2. 캡처 (Capture)”람다 바깥 스코프의 변수를 람다 내부에서 사용하려면 캡처해야 합니다.
2.1 값 캡처 [=] / [변수명]
섹션 제목: “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 참조 캡처 [&] / [&변수명]
섹션 제목: “2.2 참조 캡처 [&] / [&변수명]”int Counter = 0;
// 참조 캡처 — 외부 변수를 직접 수정 가능auto Increment = [&Counter]() { ++Counter; };Increment();Increment();std::cout << Counter; // 2
// 전체 참조 캡처 (주의: 람다 수명이 변수 수명을 초과하면 위험)auto Lambda = [&]() { ++Counter; };2.3 mutable — 값 캡처 변수 수정
섹션 제목: “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) — 이동 캡처
섹션 제목: “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 캡처
섹션 제목: “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 캡처 방식 요약
섹션 제목: “2.6 캡처 방식 요약”| 캡처 | 의미 |
|---|---|
[] | 캡처 없음 |
[=] | 모든 지역변수 값 복사 |
[&] | 모든 지역변수 참조 |
[x] | x만 값 복사 |
[&x] | x만 참조 |
[=, &x] | 기본 값 복사, x만 참조 |
[x = expr] | 초기화 캡처 (C++14) |
[this] | this 포인터 캡처 |
[*this] | 객체 전체 복사 캡처 (C++17) |
3. 제네릭 람다 (C++14)
섹션 제목: “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)
섹션 제목: “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 — 타입 소거 래퍼
섹션 제목: “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 람다 직접 사용 — 성능 비교
섹션 제목: “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. 재귀 람다
섹션 제목: “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. 실전 패턴
섹션 제목: “7. 실전 패턴”7.1 이벤트 핸들러 등록
섹션 제목: “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)
섹션 제목: “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 스코프 가드
섹션 제목: “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()은 반드시 호출됨}8. 정리
섹션 제목: “8. 정리”| 상황 | 권장 |
|---|---|
| 일회성 콜백·정렬 비교자 | auto + 람다 직접 |
| 타입을 저장·전달해야 할 때 | std::function<> |
| 외부 변수 수정 필요 | 참조 캡처 [&var] |
| 람다 수명 > 변수 수명 | 값 캡처 [var] 또는 초기화 캡처 |
| 이동 전용 타입 캡처 | 초기화 캡처 [ptr = std::move(ptr)] |
| 복잡한 const 초기화 | IIFE 패턴 |
핵심 규칙:
[&]로 전체 참조 캡처 시 람다 수명을 반드시 확인 — 댕글링 참조 위험std::function은 편리하지만 성능이 중요한 핫패스에서는 템플릿으로 대체this를 캡처하는 람다는 객체 소멸 후 호출되지 않도록 수명을 관리
9. C++20 — 템플릿 람다
섹션 제목: “9. C++20 — 템플릿 람다”C++20에서는 람다에 명시적 템플릿 파라미터를 선언할 수 있습니다.
// C++14 제네릭 람다 — 타입 추론만 가능, 타입 제약 어려움auto old_lambda = [](auto x, auto y) { return x + y; };
// C++20 템플릿 람다 — 타입 제약과 조작 가능auto new_lambda = []<typename T>(T x, T y) -> T { return x + y;};
// 같은 타입이어야 함을 강제 (auto는 다른 타입도 허용)new_lambda(1, 2); // OK: both int// new_lambda(1, 2.0); // 오류: int vs double
// 타입 정보 활용auto type_info_lambda = []<typename T>(const std::vector<T>& v) { std::cout << "Vector of " << typeid(T).name() << ", size=" << v.size() << "\n";};
type_info_lambda(std::vector<int>{1, 2, 3}); // Vector of int, size=3type_info_lambda(std::vector<double>{1.0, 2.0}); // Vector of double, size=2
// Concepts와 결합auto concept_lambda = []<std::integral T>(T a, T b) { return a + b;};concept_lambda(1, 2); // OK// concept_lambda(1.0, 2.0); // 오류: double은 integral 아님10. C++23 — Deducing this (명시적 this 파라미터)
섹션 제목: “10. C++23 — Deducing this (명시적 this 파라미터)”// 재귀 람다를 깔끔하게 작성 (std::function 불필요)auto factorial = [](this auto&& self, int n) -> int { return n <= 1 ? 1 : n * self(n - 1);};
std::cout << factorial(5); // 120
// 람다 내부에서 자기 자신 호출auto fib = [](this auto self, int n) -> int { if (n <= 1) return n; return self(n - 1) + self(n - 2);};
std::cout << fib(10); // 55
// 값/참조 캡처 구분 없이 복사본으로 호출 가능struct Counter { int count = 0;
auto make_incrementer() { // *this를 복사해서 독립적인 카운터 생성 return [*this](this auto self) mutable { return ++self.count; }; }};11. 람다 버전별 진화 요약
섹션 제목: “11. 람다 버전별 진화 요약”| C++ 버전 | 추가된 기능 |
|---|---|
| C++11 | 람다 기본 문법, 값/참조 캡처, [=], [&] |
| C++14 | 제네릭 람다(auto), 초기화 캡처([x = expr]) |
| C++17 | [*this] 복사 캡처, constexpr 람다 |
| C++20 | 템플릿 람다([]<T>), [=, this] 명시적, Concepts 제약 |
| C++23 | Deducing this(this auto self), 재귀 람다 |
// C++17: constexpr 람다constexpr auto square = [](int x) { return x * x; };static_assert(square(5) == 25); // 컴파일 타임 평가 가능
// C++20: [=, this] — [=]에서 this 암묵적 캡처 경고 해결class Widget { int value_ = 42; void setup() { // C++20 이전: [=]는 this를 암묵적으로 캡처 (deprecated) // C++20: [=, this]로 명시적 선언 auto f = [=, this]() { return value_; }; }};