[2026] 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에서 자주 쓰입니다.
문제의 코드:
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
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());
람다로 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
std::vector<Person> people = { /* ....*/ };
std::sort(people.begin(), people.end(),
{
return a.age < b.age;
});
이 글을 읽으면:
- 람다 표현식의 기본 문법을 이해할 수 있습니다.
- 캡처 방식(값, 참조)을 올바르게 사용할 수 있습니다.
- mutable, 제네릭 람다를 활용할 수 있습니다.
- 자주 겪는 에러와 해결법을 알 수 있습니다.
- 프로덕션에서의 람다 패턴을 배울 수 있습니다. 람다의 캡처와 실행 흐름을 요약하면 아래와 같습니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 람다 기초 문법
- 캡처 방식 완전 정리
- mutable과 예외 지정
- 제네릭 람다 (C++14)
- 재귀 람다
- 완전한 람다 예제 모음
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
1. 람다 기초 문법
기본 형태
람다는 캡처 [ ], 매개변수 ( ), 반환 타입 -> return_type(생략 가능), 본문 { } 네 부분으로 이루어집니다.
[capture](parameters) -> return_type {
// 함수 본문
}
최소 예제
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 복사해 붙여넣은 뒤: 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문의 식으로부터 반환 타입을 추론합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
auto add = {
return a + b; // int 반환
};
auto multiply = {
return a * b; // double 반환
};
매개변수 없는 람다
매개변수가 없으면 ()만 쓰거나, C++11 이후에서는 괄호를 아예 생략할 수 있습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
auto sayHello = {
std::cout << "Hello!\n";
};
auto sayWorld = [] {
std::cout << "World!\n";
};
즉시 실행 람다 (IIFE)
람다를 정의한 뒤 바로 (인자)를 붙여 호출하면, “한 번만 쓰는 함수”를 인라인으로 실행할 수 있습니다. IIFE(Immediately Invoked Function Expression)는 JavaScript에서 유래한 용어로, C++에서도 복잡한 초기화·스코프 분리·const 변수 초기화에 유용합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 기본 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. 캡처 방식 완전 정리
값 캡처 [=]
[=]는 람다가 정의된 시점의 주변 변수들을 값으로 복사해 둡니다. 람다가 나중에 실행될 때(예: 콜백) 참조가 끊기지 않도록 할 때 사용합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
int x = 10;
int y = 20;
auto lambda = [=]() {
std::cout << x << ", " << y << "\n"; // 10, 20
};
x = 100; // 람다 내부의 x는 변하지 않음
lambda(); // 10, 20
참조 캡처 [&]
[&]는 주변 변수를 참조로 캡처합니다. 람다 안에서 수정하면 원본이 바뀝니다. 즉시 호출할 때만 안전합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
int x = 10;
int y = 20;
auto lambda = [&]() {
x += 5;
y += 10;
};
lambda();
std::cout << x << ", " << y << "\n"; // 15, 30
선택적 캡처
캡처할 변수를 이름으로 나열하면, [x]는 값, [&y]는 참조로만 캡처합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 변수 선언 및 초기화
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만 값으로 캡처합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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]로 현재 객체를 캡처합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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로 가져올 수 있습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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을 붙이면 그 복사본은 수정 가능해지지만, 원본은 변하지 않습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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를 쓸 수 있어서, 호출될 때마다 인자 타입에 맞는 템플릿 인스턴스가 생성됩니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
auto print = {
std::cout << value << "\n";
};
print(42); // int
print(3.14); // double
print("hello"); // const char*
여러 타입 매개변수
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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에서는 람다에 템플릿 매개변수를 직접 쓸 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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을 이용한 재귀
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 방식이 더 읽기 쉽습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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;
}
실행 결과: 다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Found: Alice, 25
Count (age>=25): 3
All adult: yes
Squares: 1 4 9 16 25 36 49 64 81 100
예제 2: sort와 캡처 활용
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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와 값 캡처
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 카운터
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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: 제네릭 람다로 타입별 처리
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 이동
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 분기 초기화
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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]로 객체 전체를 복사하면 스레드가 독립된 복사본을 갖습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
증상: 프로그램이 크래시하거나, 이상한 값이 출력되거나, 릴리즈 빌드에서만 문제가 발생합니다. 원인: 참조로 캡처한 지역 변수가 스코프를 벗어나 소멸한 뒤, 람다가 실행됩니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예
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는 최종값입니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예
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로 취급됩니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예
int x = 0;
auto lambda = [x]() {
x++; // 컴파일 에러
};
// ✅ 올바른 예
auto lambda = [x]() mutable {
x++; // 복사본만 수정
};
에러 4: this 캡처 후 객체 소멸
증상: 멤버 함수에서 람다를 스레드나 큐에 넘긴 뒤, 객체가 먼저 소멸하면 댕글링 포인터 접근으로 크래시합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 위험
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: [=]로 큰 객체 불필요 복사
증상: 메모리 사용량 증가, 성능 저하.
원인: [=]는 사용하지 않는 변수까지 모두 복사할 수 있습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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]처럼 순환 참조는 불가능합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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 람다의 캡처 변수는 람다 객체 멤버입니다. 여러 스레드가 같은 람다를 공유하면 동시 수정이 발생합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 참조로 공유 → 데이터 레이스
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>로 람다를 직접 받는 편이 좋습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 매 호출마다 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. 즉시 실행 람다로 복잡 초기화
복잡한 초기화를 한 번만 수행하고, 결과만 변수에 넣을 때 즉시 실행 람다를 사용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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: 스레드에 안전하게 인자 전달
스레드에 넘기는 람다는 반드시 값 캡처로 필요한 데이터를 복사합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void processInBackground(const std::string& input) {
std::thread t([input]() {
auto result = expensiveComputation(input);
saveResult(result);
});
t.detach();
}
패턴 B: std::async와 람다
std::async에 람다를 넘길 때도 값 캡처를 사용합니다.
다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
auto future = std::async(std::launch::async, [data = prepareData()]() {
return process(data);
});
auto result = future.get();
패턴 C: ScopeGuard와 에러 처리
예외 발생 시에도 리소스를 안전하게 해제합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 패턴을 람다로 묶어 전략 패턴처럼 동작을 주입합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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로 수명을 맞춥니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 에러 핸들러 주입
재시도·로깅을 람다로 주입해 동일한 구조를 재사용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 알고리즘 커스터마이징
비교·변환 함수를 람다로 주입해 동일한 알고리즘을 다양한 용도로 사용합니다. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.