C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
이 글의 핵심
C++ 람다 기초 : 캡처·mutable·제네릭 람다와 실전 패턴. 정렬 기준을 바꾸려면 클래스를 만들어야 하나요?·실무에서 겪은 문제.
💡 초보자를 위한 순서:
[](int a, int b){ return a < b; }처럼 인자를 적는 람다부터 익힌 뒤,[=]/[&]는 “바깥 변수를 어떻게 가져올지”만 추가하면 됩니다. 스레드·비동기로 넘길 땐[&]에 지역 변수를 잡지 말고 값 캡처를 우선 의심하세요. 상세는 본문 구현 체크리스트를 보세요.
들어가며: 정렬 기준을 바꾸려면 클래스를 만들어야 하나요?
”한 줄 비교 로직인데 왜 이렇게 복잡해요?”
벡터를 나이순으로 정렬하려고 했습니다. std::sort에는 비교 함수가 필요한데, C++11 이전에는 함수 객체(functor)를 만들어야 했습니다. 한 줄짜리 비교 로직인데 클래스를 정의하고 operator()를 구현하는 것이 과했습니다.
자주 겪는 문제 시나리오들
시나리오 1: find_if에 조건을 넘기려면?
“나이가 25 이상인 첫 번째 사람”을 찾으려면 std::find_if에 predicate를 넘겨야 합니다. 별도 함수나 함수 객체를 만들면 파일 상단에 정의가 떠 있고, 호출부와 멀리 떨어져 가독성이 떨어집니다.
시나리오 2: 스레드에 지역 변수를 넘기려면?
std::thread 생성 시 함수와 인자를 넘기는데, 여러 지역 변수를 조합한 로직을 실행하려면 구조체로 묶거나 std::bind를 써야 했습니다. 람다와 캡처를 쓰면 “어떤 변수를 어떻게 쓸지” 한 곳에서 명확히 표현할 수 있습니다.
시나리오 3: 콜백이 나중에 실행될 때
버튼 클릭 핸들러나 타이머 콜백처럼 “나중에 호출될” 함수에 지역 변수를 넘기려면, 참조로 넘기면 댕글링이 되고, 값으로 복사하면 수정이 반영되지 않는 딜레마가 있습니다. 람다의 캡처 모드([=], [&], [x, &y])를 이해하면 이 상황을 안전하게 처리할 수 있습니다.
시나리오 4: STL 알고리즘마다 다른 비교/조건
sort는 나이로, find_if는 이름으로, count_if는 점수로… 각각 다른 기준이 필요할 때마다 전역 함수나 클래스를 만들면 코드가 산만해집니다. 람다는 호출 지점에서 바로 정의할 수 있어 의도가 분명합니다.
시나리오 5: 비동기 작업 완료 후 지역 변수 사용
네트워크 요청이나 파일 I/O가 끝난 뒤 콜백에서 “요청 시점의” requestId나 userId를 사용해야 합니다. 참조 캡처 [&]를 쓰면 스택이 해제된 뒤 댕글링이 되고, 값 캡처 [=]로 필요한 변수만 복사해야 합니다.
시나리오 6: 조건에 따라 다른 초기화
const 변수를 if 분기마다 다른 값으로 초기화하고 싶을 때, IIFE를 쓰면 분기 로직을 한 블록에 담아 const config = [&](){ ....}(); 형태로 깔끔하게 처리할 수 있습니다.
정의를 풀어 쓰면 람다(lambda)는 “이름 없는 작은 함수”를 호출하는 자리에서 바로 정의하는 문법입니다. 비유하면 필요한 순간에만 쓰는 임시 메모 같은 것입니다. sort·find_if·스레드 생성처럼 콜백(나중에 호출될 함수를 인자로 넘기는 방식)이 필요한 STL·API에서 자주 쓰입니다.
문제의 코드:
struct CompareByAge {
bool operator()(const Person& a, const Person& b) const {
return a.age < b.age;
}
};
std::vector<Person> people = { /* ....*/ };
std::sort(people.begin(), people.end(), CompareByAge());
람다로 해결:
// 실행 예제
std::vector<Person> people = { /* ....*/ };
std::sort(people.begin(), people.end(),
{
return a.age < b.age;
});
이 글을 읽으면:
- 람다 표현식의 기본 문법을 이해할 수 있습니다.
- 캡처 방식(값, 참조)을 올바르게 사용할 수 있습니다.
- mutable, 제네릭 람다를 활용할 수 있습니다.
- 자주 겪는 에러와 해결법을 알 수 있습니다.
- 프로덕션에서의 람다 패턴을 배울 수 있습니다. 람다의 캡처와 실행 흐름을 요약하면 아래와 같습니다.
flowchart TB
subgraph capture[캡처 시점]
A[람다 정의] --> B{캡처 모드}
B --> C[""(="] 값 복사"]
B --> D[""(&"] 참조"]
B --> E[""(x, &y"] 혼합"]
end
subgraph exec[실행 시점]
F[람다 호출] --> G{캡처 타입}
G --> H["값: 스냅샷 사용"]
G --> I["참조: 현재 값 접근"]
end
capture --> exec
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
1. 람다 기초 문법
기본 형태
람다는 캡처 [ ], 매개변수 ( ), 반환 타입 -> return_type(생략 가능), 본문 { } 네 부분으로 이루어집니다.
[capture](parameters) -> return_type {
// 함수 본문
}
최소 예제
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o lambda_basic lambda_basic.cpp && ./lambda_basic
#include <iostream>
int main() {
auto add = -> int {
return a + b;
};
int result = add(3, 5); // 8
std::cout << result << "\n";
return 0;
}
실행 결과:
8
위 코드 설명: []는 캡처가 없음을 의미합니다. (int a, int b)는 일반 함수와 동일한 매개변수입니다. -> int는 반환 타입을 명시하며, 생략하면 컴파일러가 return a + b로부터 추론합니다.
반환 타입 생략
-> return_type을 쓰지 않으면 컴파일러가 return문의 식으로부터 반환 타입을 추론합니다.
auto add = {
return a + b; // int 반환
};
auto multiply = {
return a * b; // double 반환
};
매개변수 없는 람다
매개변수가 없으면 ()만 쓰거나, C++11 이후에서는 괄호를 아예 생략할 수 있습니다.
auto sayHello = {
std::cout << "Hello!\n";
};
auto sayWorld = [] {
std::cout << "World!\n";
};
즉시 실행 람다 (IIFE)
람다를 정의한 뒤 바로 (인자)를 붙여 호출하면, “한 번만 쓰는 함수”를 인라인으로 실행할 수 있습니다. IIFE(Immediately Invoked Function Expression)는 JavaScript에서 유래한 용어로, C++에서도 복잡한 초기화·스코프 분리·const 변수 초기화에 유용합니다.
// 기본 IIFE: 즉시 계산
int result = {
return x * x;
}(5); // 25
// 복잡한 초기화를 const 변수에
auto data = {
std::vector<int> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(i * i);
}
return vec;
}();
// 스코프 분리: 임시 변수 노출 방지
int value = [&]() {
int temp1 = computeA();
int temp2 = computeB();
return temp1 + temp2; // temp1, temp2는 외부에 노출되지 않음
}();
IIFE 활용: const 분기 초기화, 스코프 분리, RAII 스코프.
2. 캡처 방식 완전 정리
값 캡처 [=]
[=]는 람다가 정의된 시점의 주변 변수들을 값으로 복사해 둡니다. 람다가 나중에 실행될 때(예: 콜백) 참조가 끊기지 않도록 할 때 사용합니다.
int x = 10;
int y = 20;
auto lambda = [=]() {
std::cout << x << ", " << y << "\n"; // 10, 20
};
x = 100; // 람다 내부의 x는 변하지 않음
lambda(); // 10, 20
참조 캡처 [&]
[&]는 주변 변수를 참조로 캡처합니다. 람다 안에서 수정하면 원본이 바뀝니다. 즉시 호출할 때만 안전합니다.
int x = 10;
int y = 20;
auto lambda = [&]() {
x += 5;
y += 10;
};
lambda();
std::cout << x << ", " << y << "\n"; // 15, 30
선택적 캡처
캡처할 변수를 이름으로 나열하면, [x]는 값, [&y]는 참조로만 캡처합니다.
// 변수 선언 및 초기화
int x = 10;
int y = 20;
int z = 30;
auto lambda = [x, &y]() {
std::cout << x << ", " << y << "\n";
// std::cout << z; // ❌ 에러: z 캡처 안 됨
};
혼합 캡처
[=, &y]는 기본은 값 캡처이고 y만 참조로, [&, x]는 기본은 참조이고 x만 값으로 캡처합니다.
int x = 10;
int y = 20;
int z = 30;
auto lambda1 = [=, &y]() {
std::cout << x << ", " << y << ", " << z << "\n";
};
auto lambda2 = [&, x]() {
std::cout << x << ", " << y << ", " << z << "\n";
};
this 캡처
멤버 함수 안에서 람다를 쓰고 멤버 변수에 접근하려면 [this]로 현재 객체를 캡처합니다.
class Counter {
int count = 0;
public:
void increment() {
auto lambda = [this]() {
count++;
};
lambda();
}
int getCount() const { return count; }
};
초기화 캡처 (C++14)
[이름 = 식]은 람다 전용 변수를 만들어 식의 결과로 초기화합니다. [p = std::move(ptr)]처럼 move로 가져올 수 있습니다.
int x = 10;
auto lambda = [y = x + 5]() {
std::cout << y << "\n"; // 15
};
auto ptr = std::make_unique<int>(42);
auto lambda2 = [p = std::move(ptr)]() {
std::cout << *p << "\n";
};
캡처 모드 비교표
| 캡처 문법 | 의미 | 수명 안전 | 사용 시점 |
|---|---|---|---|
[] | 아무것도 캡처 안 함 | 항상 안전 | 외부 변수 불필요 |
[=] | 모든 변수 값 복사 | 나중 호출 시 안전 | 비동기·스레드·저장용 콜백 |
[&] | 모든 변수 참조 | 즉시 호출만 안전 | 동기 콜백, sort·find_if |
[x] | x만 값 복사 | 나중 호출 시 안전 | 필요한 변수만 선택 |
[&y] | y만 참조 | 즉시 호출만 안전 | 수정 필요할 때 |
[=, &y] | 기본 값, y만 참조 | y 수명 주의 | 대부분 복사, y만 갱신 |
[&, x] | 기본 참조, x만 값 | x는 안전 | 대부분 참조, x만 스냅샷 |
[this] | 현재 객체 포인터 | 객체 수명 주의 | 멤버 함수 내부 |
[*this] (C++17) | 객체 전체 복사 | 나중 호출 시 안전 | 스레드로 객체 넘길 때 |
[p = std::move(ptr)] | move로 초기화 | 소유권 이전 | unique_ptr, 큰 객체 |
3. mutable과 예외 지정
mutable 람다
값으로 캡처한 변수는 람다 안에서 const로 취급되어 수정할 수 없습니다. mutable을 붙이면 그 복사본은 수정 가능해지지만, 원본은 변하지 않습니다.
int x = 0;
auto lambda1 = [x]() {
// x++; // ❌ 에러: const
std::cout << x << "\n";
};
auto lambda2 = [x]() mutable {
x++; // ✅ OK (복사본 수정)
std::cout << x << "\n";
};
lambda2(); // 1
lambda2(); // 2
std::cout << x << "\n"; // 0 (원본은 변하지 않음)
위 코드 설명: mutable은 람다의 operator()를 비const로 만듭니다. 값 캡처된 변수는 람다 객체의 멤버로 저장되며, 호출할 때마다 그 복사본이 갱신됩니다. 원본 x는 영향을 받지 않습니다.
noexcept 지정
noexcept를 붙이면 이 람다가 예외를 던지지 않는다고 선언합니다.
auto lambda = noexcept {
return 42;
};
속성 지정
auto lambda = [[nodiscard]] {
return 42;
};
4. 제네릭 람다 (C++14)
auto 매개변수
C++14부터 람다 매개변수에 auto를 쓸 수 있어서, 호출될 때마다 인자 타입에 맞는 템플릿 인스턴스가 생성됩니다.
auto print = {
std::cout << value << "\n";
};
print(42); // int
print(3.14); // double
print("hello"); // const char*
여러 타입 매개변수
auto add = {
return a + b;
};
std::cout << add(1, 2) << "\n"; // 3
std::cout << add(1.5, 2.5) << "\n"; // 4.0
std::cout << add(std::string("Hello"), std::string(" World")) << "\n";
템플릿 람다 (C++20)
C++20에서는 람다에 템플릿 매개변수를 직접 쓸 수 있습니다.
auto lambda = []<typename T>(T value) {
std::cout << typeid(T).name() << ": " << value << "\n";
};
lambda(42); // int: 42
lambda(3.14); // double: 3.14
4.5. 재귀 람다 (Recursive Lambda)
람다는 이름이 없어서 자기 자신을 직접 호출할 수 없습니다. 재귀를 구현하려면 std::function에 담거나, 초기화 캡처로 자기 자신을 캡처해야 합니다.
std::function을 이용한 재귀
#include <functional>
#include <iostream>
int main() {
std::function<int(int)> factorial;
factorial = [&factorial](int n) -> int {
if (n <= 1) return 1;
return n * factorial(n - 1);
};
std::cout << factorial(5) << "\n"; // 120
return 0;
}
주의: factorial을 참조로 캡처해야 합니다. 값 캡처 [=]를 쓰면 복사 시점에 아직 초기화되지 않은 factorial이 복사되어 댕글링이 됩니다.
Y 컴비네이터 스타일 (고급)
std::function 없이 재귀를 구현하려면 고차 함수를 이용합니다. 실무에서는 std::function 방식이 더 읽기 쉽습니다.
// Y 컴비네이터: 람다가 자기 자신을 인자로 받음
auto factorial = -> int {
if (n <= 1) return 1;
return n * self(self, n - 1);
};
std::cout << factorial(factorial, 5) << "\n"; // 120
재귀 람다 사용 시나리오
- 트리/그래프 순회: 노드 방문 시 자식에게 같은 람다 전달
- JSON/XML 파싱: 중첩 구조 재귀 처리
- 수학적 정의: 팩토리얼, 피보나치 등
6. 완전한 람다 예제 모음
예제 1: STL 알고리즘 (find_if, count_if, transform)
// g++ -std=c++17 -o lambda_stl lambda_stl.cpp && ./lambda_stl
#include <algorithm>
#include <iostream>
#include <vector>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 25}, {"Bob", 30}, {"Charlie", 20}, {"Diana", 25}
};
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto it = std::find_if(people.begin(), people.end(),
{ return p.age >= 25; });
if (it != people.end())
std::cout << "Found: " << it->name << ", " << it->age << "\n";
int count = std::count_if(people.begin(), people.end(),
{ return p.age >= 25; });
std::cout << "Count (age>=25): " << count << "\n";
bool allAdult = std::all_of(people.begin(), people.end(),
{ return p.age >= 18; });
std::cout << "All adult: " << (allAdult ? "yes" : "no") << "\n";
std::vector<int> squares;
std::transform(numbers.begin(), numbers.end(),
std::back_inserter(squares),
{ return x * x; });
std::cout << "Squares: ";
for (int s : squares) std::cout << s << " ";
std::cout << "\n";
return 0;
}
실행 결과:
Found: Alice, 25
Count (age>=25): 3
All adult: yes
Squares: 1 4 9 16 25 36 49 64 81 100
예제 2: sort와 캡처 활용
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {
{"Alice", 25}, {"Bob", 30}, {"Charlie", 20}
};
int minAge = 18;
std::sort(people.begin(), people.end(),
{
return a.age < b.age;
});
std::sort(people.begin(), people.end(),
[minAge](const Person& a, const Person& b) {
return a.age < b.age && a.age >= minAge;
});
예제 3: std::thread와 값 캡처
// g++ -std=c++17 -pthread -o lambda_thread lambda_thread.cpp && ./lambda_thread
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
int multiplier = 10;
std::thread t([data, multiplier]() {
for (int x : data) {
std::cout << x * multiplier << " ";
}
std::cout << "\n";
});
t.join();
return 0;
}
실행 결과:
10 20 30 40 50
예제 4: mutable 카운터
int main() {
auto counter = [n = 0]() mutable {
return ++n;
};
std::cout << counter() << "\n"; // 1
std::cout << counter() << "\n"; // 2
std::cout << counter() << "\n"; // 3
}
예제 5: 제네릭 람다로 타입별 처리
auto process = {
if constexpr (std::is_integral_v<decltype(value)>) {
return value * 2;
} else if constexpr (std::is_floating_point_v<decltype(value)>) {
return value * 1.5;
} else {
return value;
}
};
std::cout << process(10) << "\n"; // 20
std::cout << process(10.0) << "\n"; // 15
예제 6: 초기화 캡처로 unique_ptr 이동
#include <memory>
#include <iostream>
int main() {
auto ptr = std::make_unique<int>(42);
// [p = std::move(ptr)]: 소유권 이전
auto lambda = [p = std::move(ptr)]() {
std::cout << *p << "\n";
};
lambda(); // 42
// ptr은 이제 nullptr (이동됨)
return 0;
}
예제 8: IIFE로 const 분기 초기화
#include <iostream>
#include <string>
int main() {
bool useCache = true;
// const 변수를 조건에 따라 다르게 초기화
const std::string config = [&]() {
if (useCache) {
return std::string("cache_enabled");
} else {
return std::string("cache_disabled");
}
}();
std::cout << config << "\n"; // cache_enabled
return 0;
}
예제 8: [*this]로 객체 복사 (C++17)
[this]는 객체 소멸 시 댕글링 위험이 있습니다. [*this]로 객체 전체를 복사하면 스레드가 독립된 복사본을 갖습니다.
#include <thread>
#include <iostream>
struct Worker {
int id = 0;
void runAsync() {
// [*this]: 객체 복사, 스레드가 종료될 때까지 안전
std::thread t([*this]() {
std::cout << "Worker " << id << " running\n";
});
t.detach();
}
};
int main() {
Worker w;
w.id = 42;
w.runAsync();
// main 종료 후에도 스레드는 복사본으로 동작
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 0;
}
7. 자주 발생하는 에러와 해결법
에러 1: 댕글링 참조 (Dangling Reference)
증상: 프로그램이 크래시하거나, 이상한 값이 출력되거나, 릴리즈 빌드에서만 문제가 발생합니다. 원인: 참조로 캡처한 지역 변수가 스코프를 벗어나 소멸한 뒤, 람다가 실행됩니다.
// ❌ 잘못된 예
std::function<void()> createBroken() {
int x = 42;
return [&x]() { std::cout << x << "\n"; }; // x는 createBroken() 종료 시 소멸
}
// ✅ 올바른 예
std::function<void()> createSafe() {
int x = 42;
return [x]() { std::cout << x << "\n"; };
}
에러 2: for 루프 변수 캡처
증상: 스레드나 콜백에서 루프 인덱스 i가 항상 마지막 값만 나옵니다.
원인: [&i]로 참조 캡처하면 모든 람다가 같은 i를 가리키고, 루프가 끝난 뒤 i는 최종값입니다.
// ❌ 잘못된 예
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([&i]() {
std::cout << i << "\n"; // 모두 5 또는 undefined
});
}
// ✅ 올바른 예
for (int i = 0; i < 5; ++i) {
threads.emplace_back([i]() {
std::cout << i << "\n"; // 0, 1, 2, 3, 4
});
}
에러 3: mutable 없이 값 캡처 수정 시도
증상: error: increment of read-only variable 'x' 컴파일 에러.
원인: 값 캡처된 변수는 람다 내부에서 const로 취급됩니다.
// ❌ 잘못된 예
int x = 0;
auto lambda = [x]() {
x++; // 컴파일 에러
};
// ✅ 올바른 예
auto lambda = [x]() mutable {
x++; // 복사본만 수정
};
에러 4: this 캡처 후 객체 소멸
증상: 멤버 함수에서 람다를 스레드나 큐에 넘긴 뒤, 객체가 먼저 소멸하면 댕글링 포인터 접근으로 크래시합니다.
// ❌ 위험
class Worker {
void start() {
std::thread t([this]() {
this->doWork(); // Worker 소멸 후 호출 시 UB
});
t.detach();
}
};
// ✅ 안전: shared_from_this 또는 [*this] (C++17)
class Worker : public std::enable_shared_from_this<Worker> {
void start() {
auto self = shared_from_this();
std::thread t([self]() { self->doWork(); });
t.detach();
}
};
에러 5: [=]로 큰 객체 불필요 복사
증상: 메모리 사용량 증가, 성능 저하.
원인: [=]는 사용하지 않는 변수까지 모두 복사할 수 있습니다.
std::string big(10000, 'x');
int threshold = 10;
// ❌ big 전체 복사
auto bad = [=]() { return big.size() > threshold; };
// ✅ 참조 + 값
auto good = [&big, threshold]() { return big.size() > threshold; };
에러 6: 초기화 캡처 순서 오류
증상: error: 'x' was not declared in this scope 또는 예상치 못한 값.
원인: 초기화 캡처 [a = b]에서 b는 캡처 리스트보다 앞에 선언되어 있어야 합니다. [a = b, b = a]처럼 순환 참조는 불가능합니다.
int x = 10;
// ❌ y가 아직 정의되지 않음
// auto bad = [z = y, y = x]() { return z + y; };
// ✅ 올바른 순서
auto good = [y = x]() { return y * 2; };
에러 7: std::function 시그니처 불일치
증상: 컴파일 에러 또는 런타임 크래시.
원인: std::function의 반환/인자 타입과 람다가 맞지 않을 때.
// ❌ std::function<int()> fn = { std::cout << "hi"; }; // void 반환
// ✅ std::function<void()> fn = { std::cout << "hi\n"; };
에러 8: mutable 람다를 여러 스레드에서 동시 호출
증상: 데이터 레이스, UB.
원인: mutable 람다의 캡처 변수는 람다 객체 멤버입니다. 여러 스레드가 같은 람다를 공유하면 동시 수정이 발생합니다.
// ❌ 참조로 공유 → 데이터 레이스
auto counter = [n = 0]() mutable { return ++n; };
std::thread t1([&counter]() { for (int i = 0; i < 100; ++i) counter(); });
std::thread t2([&counter]() { for (int i = 0; i < 100; ++i) counter(); });
// ✅ 스레드마다 람다 복사
std::thread t1([counter]() mutable { /* ....*/ });
std::thread t2([counter]() mutable { /* ....*/ });
에러 요약표
| 에러 | 원인 | 해결법 |
|---|---|---|
| 댕글링 참조 | 참조 캡처 후 스코프 종료 | 값 캡처 [x] |
| for 루프 i | [&i] 참조 캡처 | [i] 값 캡처 |
| 값 캡처 수정 | const 취급 | mutable 추가 |
| this 소멸 | 객체가 먼저 소멸 | shared_from_this, [*this] |
| 불필요 복사 | [=] 과다 사용 | 선택 캡처 [&big, x] |
| 초기화 캡처 순서 | 미정의 변수 참조 | 선언 순서 확인 |
| std::function 타입 | 시그니처 불일치 | 반환/인자 타입 일치 |
| mutable + 멀티스레드 | 공유 람다 객체 수정 | 스레드마다 복사 |
성능 비교: 람다 vs std::function vs 함수 포인터
| 방식 | 인라인 가능 | 힙 할당 | 오버헤드 | 사용 시점 |
|---|---|---|---|---|
| 람다 (템플릿 전달) | ✅ | ❌ | 없음 | 콜백을 한 번만 받을 때 |
| 람다 (std::function 저장) | ❌ | ✅ 가능 | 호출당 간접 호출 | 타입 소거 필요 시 |
| 함수 포인터 | ❌ | ❌ | 간접 호출 | C API 연동 |
| 함수 객체 | ✅ | ❌ | 없음 | 상태가 필요할 때 |
실무 팁: std::sort(v.begin(), v.end(), { return a < b; })처럼 람다를 템플릿 인자로 직접 넘기면 컴파일러가 인라인 최적화를 수행합니다. std::function<bool(int,int)>로 받으면 타입이 소거되어 인라인이 어렵고, 작은 람다도 힙에 저장될 수 있습니다. |
8. 베스트 프랙티스
1. 캡처 선택 가이드
- 즉시 호출(sort, find_if):
[&]또는[=]모두 안전 - 나중 호출(스레드, async, 콜백 저장):
[=]또는[x, y]값 캡처 - 큰 객체:
[&]또는[s = std::move(str)]move 캡처
2. std::function 대신 템플릿
std::function은 힙 할당을 할 수 있어 오버헤드가 있습니다. 콜백을 한 번만 받는다면 template<typename F>로 람다를 직접 받는 편이 좋습니다.
// ❌ 매 호출마다 function 오버헤드
void sortWithFunc(std::vector<int>& v, std::function<bool(int,int)> cmp);
// ✅ 인라인 가능, 오버헤드 없음
template <typename Compare>
void sortWithLambda(std::vector<int>& v, Compare cmp) {
std::sort(v.begin(), v.end(), cmp);
}
3. 필요한 것만 캡처
[=]는 사용하지 않는 변수까지 복사할 수 있으므로, 필요한 변수만 [a, &b]로 선택 캡처합니다.
4. noexcept 람다
예외를 던지지 않는 람다에 noexcept를 붙이면, 이동 연산 등에서 컴파일러가 더 나은 코드를 생성할 수 있습니다.
auto safe = noexcept { return 42; };
5. 즉시 실행 람다로 복잡 초기화
복잡한 초기화를 한 번만 수행하고, 결과만 변수에 넣을 때 즉시 실행 람다를 사용합니다.
auto config = [&]() {
Config c;
c.loadFromFile("config.json");
c.merge(defaults);
return c;
}();
6. 람다 본문은 짧게 유지
5줄을 넘어가면 별도 함수나 auto compareItems = ...로 분리해 가독성을 높입니다.
7. 캡처할 변수는 최소화
[=]나 [&]는 모든 변수를 캡처하므로, 나중에 변수가 추가되면 의도치 않게 캡처될 수 있습니다. [a, &b]처럼 필요한 것만 명시합니다.
8. 재귀 람다는 std::function 또는 명명된 함수로
재귀가 복잡해지면 일반 함수가 더 읽기 쉽습니다. 람다로 할 때는 std::function에 담고 참조 캡처 [&f]를 사용합니다.
9. 프로덕션 패턴
패턴 A: 스레드에 안전하게 인자 전달
스레드에 넘기는 람다는 반드시 값 캡처로 필요한 데이터를 복사합니다.
void processInBackground(const std::string& input) {
std::thread t([input]() {
auto result = expensiveComputation(input);
saveResult(result);
});
t.detach();
}
패턴 B: std::async와 람다
std::async에 람다를 넘길 때도 값 캡처를 사용합니다.
auto future = std::async(std::launch::async, [data = prepareData()]() {
return process(data);
});
auto result = future.get();
패턴 C: ScopeGuard와 에러 처리
예외 발생 시에도 리소스를 안전하게 해제합니다.
template <typename Func>
class ScopeGuard {
Func func;
bool active = true;
public:
ScopeGuard(Func f) : func(std::move(f)) {}
~ScopeGuard() { if (active) func(); }
void dismiss() { active = false; }
};
void writeWithBackup(const std::string& path, const std::string& data) {
std::string tmpPath = path + ".tmp";
std::ofstream out(tmpPath);
if (!out) throw std::runtime_error("Cannot open " + tmpPath);
auto guard = ScopeGuard([tmpPath]() { std::filesystem::remove(tmpPath); });
out << data;
out.close();
std::filesystem::rename(tmpPath, path);
guard.dismiss();
}
패턴 D: 조건부 로직 캡슐화
반복되는 if-else 패턴을 람다로 묶어 전략 패턴처럼 동작을 주입합니다.
template <typename OnSuccess, typename OnFailure>
void tryOperation(OnSuccess&& onOk, OnFailure&& onFail) {
if (doSomething()) {
onOk();
} else {
onFail();
}
}
tryOperation(
{ log("OK"); commit(); },
{ log("Failed"); rollback(); }
);
패턴 E: 콜백 저장 시 수명 관리
버튼 클릭처럼 나중에 호출될 동작을 저장할 때, 콜백이 객체보다 오래 살 수 있으면 값 캡처나 shared_ptr로 수명을 맞춥니다.
class Button {
std::function<void()> onClick;
public:
void setOnClick(std::function<void()> callback) {
onClick = std::move(callback);
}
void click() {
if (onClick) onClick();
}
};
int main() {
Button button;
int clickCount = 0;
button.setOnClick([clickCount]() mutable {
clickCount++;
std::cout << "Clicked " << clickCount << " times\n";
});
button.click();
button.click();
}
주의: clickCount를 값 캡처하면 복사본이 들어가 main의 값은 변하지 않습니다. 상태 공유가 필요하면 std::shared_ptr나 클래스 멤버를 사용하세요.
패턴 F: 지연 계산 (Lazy Evaluation)
비용이 큰 계산을 필요할 때만 수행합니다.
auto getExpensiveResult = [&]() { return computeHeavyStuff(data); };
if (needResult) use(getExpensiveResult());
패턴 G: 에러 핸들러 주입
재시도·로깅을 람다로 주입해 동일한 구조를 재사용합니다.
template <typename Op, typename OnError>
auto tryWithRetry(Op op, OnError onError, int maxRetries = 3) {
for (int i = 0; i < maxRetries; ++i) {
try { return op(); }
catch (const std::exception& e) {
onError(i, e.what());
if (i == maxRetries - 1) throw;
}
}
throw std::runtime_error("Unreachable");
}
// 사용: tryWithRetry([&](){ return fetch(url); }, { ....});
패턴 H: 알고리즘 커스터마이징
비교·변환 함수를 람다로 주입해 동일한 알고리즘을 다양한 용도로 사용합니다.
std::sort(users.begin(), users.end(),
{ return a.lastLogin > b.lastLogin; });
std::transform(users.begin(), users.end(), std::back_inserter(names),
{ return u.name; });
10. 구현 체크리스트
람다를 도입할 때 확인할 항목입니다.
- 캡처 모드: 즉시 호출인가, 나중 호출인가에 따라
[=]vs[&]선택 - 수명 안전: 참조 캡처 시 람다가 참조보다 오래 살지 않는지 확인
- for 루프:
[i]값 캡처,[&i]사용 금지 - mutable: 값 캡처 수정 시
mutable추가 - this 캡처: 객체 수명이 람다보다 길어지는지 확인
- 큰 객체:
[&]또는[s = std::move(str)]사용 - std::function: 저장이 필요할 때만 사용, 그 외에는 템플릿으로 전달
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
이 글에서 다루는 키워드 (관련 검색어)
C++ 람다, 람다 표현식, 캡처 [=] [&], mutable, 제네릭 람다, sort 람다, find_if, 댕글링 참조 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 문법 | 용도 |
|---|---|---|
| 값 캡처 | [=] | 모든 변수 복사 |
| 참조 캡처 | [&] | 모든 변수 참조 |
| 선택 캡처 | [x, &y] | x 복사, y 참조 |
| this 캡처 | [this] | 멤버 접근 |
| 초기화 캡처 | [x = expr] | 새 변수 생성 |
| mutable | mutable | 값 캡처 수정 |
| 제네릭 | “ | 모든 타입 |
| 핵심 원칙: |
- 짧은 로직은 람다
- 참조 캡처 주의 (수명)
- 큰 객체는 참조 또는 move
- STL 알고리즘과 함께 사용
- 나중 호출 시 값 캡처
초보자를 위한 체크리스트
-
std::sort등에 넘길 비교 람다가bool반환인지 (<연산자 규약) -
[&]로 잡은 변수가 람다보다 먼저 파괉되지 않는지 (스레드·콜백 지연 호출) -
mutable은 값 캡처를 람다 안에서 바꿀 때만 필요한지 확인했는가?
💡 초보자 팁: 막히면 10절 구현 체크리스트부터 위에서 아래로 체크해 보세요.
참고 자료
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++11 람다 기초 완벽 가이드. [=]·[&] 캡처, mutable, 제네릭 람다, sort·find_if·스레드에서 람다 활용, 댕글링 참조·for 루프 캡처 등 자주 겪는 에러와 해결법. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
Q. 람다와 일반 함수의 성능 차이는?
A. 람다는 컴파일러가 인라인으로 최적화할 수 있어, std::function에 담지 않고 템플릿으로 직접 전달하면 일반 함수와 거의 동일한 성능입니다. std::function은 타입 소거를 위해 힙 할당을 할 수 있어 오버헤드가 있습니다.
한 줄 요약: 람다로 한 곳에서만 쓰는 함수를 인라인 정의해 STL과 콜백에 활용할 수 있습니다. 다음으로 STL 알고리즘(#10-3)과 람다 표현식 심화(#13-1)를 읽어보면 좋습니다.
다음 글: C++ 실전 가이드 #10-2: STL map과 set
이전 글: C++ 실전 가이드 #09-3: 가변 인자 템플릿
관련 글
- C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
- C++ vector 성능 |
- C++ map vs unordered_map (STL 시리즈) |
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 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 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.