Skip to content

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; });

[캡처](파라미터) 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; };

람다 바깥 스코프의 변수를 람다 내부에서 사용하려면 캡처해야 합니다.

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; }; // 모든 지역변수 복사
int Counter = 0;
// 참조 캡처 — 외부 변수를 직접 수정 가능
auto Increment = [&Counter]() { ++Counter; };
Increment();
Increment();
std::cout << Counter; // 2
// 전체 참조 캡처 (주의: 람다 수명이 변수 수명을 초과하면 위험)
auto Lambda = [&]() { ++Counter; };

값 캡처된 변수는 기본적으로 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으로 초기화
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(); }
// };
}
};
캡처의미
[]캡처 없음
[=]모든 지역변수 값 복사
[&]모든 지역변수 참조
[x]x만 값 복사
[&x]x만 참조
[=, &x]기본 값 복사, x만 참조
[x = expr]초기화 캡처 (C++14)
[this]this 포인터 캡처
[*this]객체 전체 복사 캡처 (C++17)

파라미터 타입에 auto를 사용해 템플릿처럼 동작합니다.

// auto 파라미터 — 컴파일러가 타입 추론
auto Print = [](auto Value)
{
std::cout << Value << "\n";
};
Print(42); // int
Print(3.14); // double
Print("hello"); // const char*
// 복수 auto 파라미터
auto Add = [](auto A, auto B) { return A + B; };
auto R1 = Add(1, 2); // int
auto R2 = Add(1.5, 2.5); // double
auto 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;
});

정의와 동시에 호출하는 패턴입니다. 복잡한 초기화 로직을 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";
}
}();

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); }
}

람다는 자기 자신을 이름으로 참조할 수 없어 재귀가 까다롭습니다.

// 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);
// };

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(); // 클릭 횟수: 1
// 작업 큐에 람다 저장 후 나중에 실행
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(); // 나중에 일괄 실행
// 람다로 스코프 종료 시 실행할 작업 등록
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를 캡처하는 람다는 객체 소멸 후 호출되지 않도록 수명을 관리