C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
이 글의 핵심
C++ 람다 기초 완벽 가이드에 대한 실전 가이드입니다. 캡처·mutable·제네릭 람다와 실전 패턴 등을 예제와 함께 상세히 설명합니다.
들어가며: 정렬 기준을 바꾸려면 클래스를 만들어야 하나요?
”한 줄 비교 로직인데 왜 이렇게 복잡해요?”
벡터를 나이순으로 정렬하려고 했습니다. 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
목차
- 람다 기초 문법
- 캡처 방식 완전 정리
- mutable과 예외 지정
- 제네릭 람다 (C++14)
- 재귀 람다
- 완전한 람다 예제 모음
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
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 알고리즘과 함께 사용
- 나중 호출 시 값 캡처
참고 자료
자주 묻는 질문 (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 람다와 함께 쓰기 (실전 패턴)