C++ 람다 표현식: 캡처·mutable·제네릭 람다부터 deducing this 재귀까지
핵심 개념: sort, find_if에 넘기는 짧은 로직은 람다 한 블록이 가장 읽기 좋습니다. [=] / [&]는 “바깥 변수를 어떻게 가져올지”만 추가하는 문법입니다. 스레드·지연 콜백에는 [&]로 지역 변수를 캡처하지 말고 [=] 또는 명시적 값 캡처를 사용하라. 이 글은 기초 문법부터 C++23 deducing this까지 한 번에 다루며, 초기화 캡처·완벽 전달 같은 심화 주제는 람다 심화(#10-2)에서 이어집니다.
들어가며: 함수 객체가 너무 복잡하다
벡터를 정렬할 때 커스텀 비교 함수가 필요했습니다. 하지만 함수 객체를 만드는 게 너무 번거로웠습니다.
find_if 조건, 정렬 기준, 콜백: 람다가 필요한 순간
find_if에 조건을 넘기려면?
“나이가 25 이상인 첫 번째 사람”을 찾으려면 std::find_if에 predicate를 넘겨야 합니다. 별도 함수나 함수 객체를 만들면 파일 상단에 정의가 떠 있으며, 호출부와 멀리 떨어져 있어서 가독성이 떨어집니다.
스레드에 지역 변수를 넘기려면?
std::thread 생성 시 함수와 인자를 넘기는데, 여러 지역 변수를 조합한 로직을 실행하려면 구조체로 묶거나 std::bind를 써야 했습니다. 람다와 캡처를 쓰면 “어떤 변수를 어떻게 쓸지” 한 곳에서 명확히 표현할 수 있습니다.
콜백이 나중에 실행될 때
버튼 클릭 핸들러나 타이머 콜백처럼 “나중에 호출될” 함수에 지역 변수를 넘기려면, 참조로 넘기면 댕글링이 되고, 값으로 복사하면 수정이 반영되지 않는 딜레마가 있습니다. 람다의 캡처 모드([=], [&], [x, &y])를 이해하면 이 상황을 안전하게 처리할 수 있습니다.
비동기 작업 완료 후 지역 변수 사용
네트워크 요청이나 파일 I/O가 끝난 뒤 콜백에서 “요청 시점의” requestId나 userId를 사용해야 합니다. 참조 캡처 [&]를 쓰면 요청을 보낸 함수의 스택이 해제된 뒤 댕글링이 되므로, 필요한 변수만 값으로 복사해 두어야 합니다.
조건에 따라 다른 초기화
const 변수를 if 분기마다 다른 값으로 초기화하고 싶을 때, 즉시 실행 람다(IIFE)를 쓰면 분기 로직을 한 블록에 담아 const config = [&](){ ... }(); 형태로 처리할 수 있습니다.
정의를 풀어 쓰면 람다(lambda—이름 없이 한 곳에서만 쓰는 작은 함수를 정의하는 문법. 비유하면 필요한 순간에만 쓰는 임시 메모 같은 것)는 “이름 없는 작은 함수”를 호출하는 자리에서 바로 정의하는 문법입니다. “한 번만 쓰는 작은 함수”를 호출 지점에 바로 정의할 수 있어서, sort·find_if·스레드 생성처럼 콜백(나중에 호출될 함수를 인자로 넘기는 방식)이 필요한 STL(Standard Template Library)·API(Application Programming Interface)에서 자주 사용됩니다. 캡처로 주변 변수를 넘기되, 람다 수명이 콜백보다 길어질 수 있으면 참조 캡처는 위험하므로 값/참조 선택을 신경 쓰는 것이 좋습니다.
참조 캡처 [&] 사용 시 주의: 람다가 나중에 비동기로 실행되거나(예: 새 스레드, 타이머 콜백) 다른 컨테이너에 저장될 때, [&]로 캡처한 지역 변수는 이미 스코프를 벗어나 파괴된 상태일 수 있습니다. 그때 람다가 실행되면 댕글링 참조로 undefined behavior(정의되지 않은 동작, UB)가 됩니다. “지금 바로 호출되는” 콜백(예: sort의 비교자)에서는 참조 캡처가 안전하며, “나중에 호출될 수 있는” 콜백에서는 값 캡처 [=] 또는 필요한 변수만 값으로 [x, y] 캡처하는 편이 안전합니다.
문제의 코드에서는 std::sort에 비교 기준을 넘기기 위해 CompareByAge 구조체를 만들고 operator()를 구현했습니다. 정렬 기준이 하나일 때는 이렇게 클래스를 정의하는 것이 과한 경우가 많고, 비교 로직을 바꿀 때마다 별도 타입을 수정해야 합니다. 람다를 쓰면 호출하는 자리에서 “이름 없는 함수”를 바로 넣을 수 있어, Person의 age로 비교한다는 의도가 한눈에 들어오고, 나중에 name으로 바꾸는 것도 람다 본문만 고치면 됩니다. sort의 세 번째 인자로 넘기는 함수 객체는 “두 원소를 받아서 앞이 뒤보다 작으면 true”를 반환하면 됩니다.
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(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
이 글을 읽으면:
- 람다 표현식의 기본 문법을 이해할 수 있습니다.
- 캡처 방식(값, 참조)을 올바르게 사용할 수 있습니다.
- 실전에서 람다를 효과적으로 활용할 수 있습니다.
- 람다의 성능과 제약을 이해할 수 있습니다. 람다의 캡처와 실행 흐름을 요약하면 아래와 같습니다.
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
람다 문법: 캡처, 매개변수, 반환 타입
기본 문법
람다는 캡처 [ ], 매개변수 ( ), 반환 타입 -> 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 a, int b) -> int {
return a + b;
};
int result = add(3, 5); // 8
std::cout << result << "\n";
return 0;
}
실행 결과: 8 이 한 줄 출력됩니다.
반환 타입 생략
-> return_type을 쓰지 않으면 컴파일러가 return문의 식으로부터 반환 타입을 추론합니다. 본문에 return이 하나만 있고 식이 단순하면 생략하는 경우가 많고, 여러 return이 있으면 모두 같은 타입이어야 합니다.
// 반환 타입 자동 추론
auto add = [](int a, int b) {
return a + b; // int 반환
};
auto multiply = [](double a, double b) {
return a * b; // double 반환
};
매개변수 없는 람다
매개변수가 없으면 ()만 쓰거나, C++11 이후에서는 괄호를 아예 생략할 수 있습니다. [] { ... }처럼 쓰면 “인자를 받지 않는 람다”입니다.
auto sayHello = []() {
std::cout << "Hello!\n";
};
sayHello(); // Hello!
// 괄호 생략 가능
auto sayWorld = [] {
std::cout << "World!\n";
};
즉시 실행 (IIFE)
람다를 정의한 뒤 바로 (인자)를 붙여 호출하면, “한 번만 쓰는 함수”를 인라인으로 실행할 수 있습니다. IIFE(Immediately Invoked Function Expression)는 JavaScript에서 온 용어로, C++에서는 복잡한 초기화 식을 한 번만 계산해 변수에 넣을 때, 임시 변수를 바깥 스코프에 노출하고 싶지 않을 때, 그리고 const 변수를 분기에 따라 다르게 초기화할 때 특히 유용합니다.
// 람다를 정의하고 즉시 호출
int result = [](int x) {
return x * x;
}(5); // 25
// 초기화에 사용
auto data = []() {
std::vector<int> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(i * i);
}
return vec;
}();
// 스코프 분리: temp1, temp2는 바깥에 노출되지 않음
int value = [&]() {
int temp1 = computeA();
int temp2 = computeB();
return temp1 + temp2;
}();
// const 변수를 조건에 따라 다르게 초기화
bool useCache = true;
const std::string config = [&]() {
if (useCache) return std::string("cache_enabled");
return std::string("cache_disabled");
}();
const 분기 초기화는 IIFE를 쓰는 가장 흔한 이유입니다. IIFE 없이 쓰면 std::string config;로 선언한 뒤 if 안에서 대입해야 해서 const를 포기하게 되고, 삼항 연산자로 쓰기엔 분기가 복잡한 경우가 많습니다. 여러 return이 있을 때는 모두 같은 타입이어야 하므로 위 예제처럼 std::string(...)으로 타입을 맞춰 줍니다("..." 리터럴만 반환하면 const char*로 추론됩니다).
[=], [&], this, 초기화 캡처 비교
값 캡처 [=]
[=]는 람다가 정의된 시점의 주변 변수들을 값으로 복사해 둡니다. 람다 안에서 쓰는 x, y는 그때의 스냅샷이므로, 나중에 바깥에서 x, y를 바꿔도 람다를 호출하면 예전 값이 나옵니다. 람다가 나중에 실행될 때(예: 콜백) 참조가 끊기지 않도록 할 때 값 캡처를 씁니다.
int x = 10;
int y = 20;
// 모든 외부 변수를 값으로 캡처
auto lambda = [=]() {
std::cout << x << ", " << y << "\n"; // 10, 20
};
x = 100; // 람다 내부의 x는 변하지 않음
lambda(); // 10, 20
참조 캡처 [&]
[&]는 주변 변수를 참조로 캡처합니다. 람다 안에서 x, y를 수정하면 원본이 바뀌고, 람다가 나중에 실행되면 그때의 x, y 값을 봅니다. 람다가 스코프를 벗어난 뒤에 호출될 수 있으면 참조가 이미 무효일 수 있어서(댕글링), 수명을 꼭 확인해야 합니다.
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;
// x는 값, y는 참조, z는 캡처 안 함
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;
// 기본은 값, y만 참조
auto lambda1 = [=, &y]() {
std::cout << x << ", " << y << ", " << z << "\n";
};
// 기본은 참조, x만 값
auto lambda2 = [&, x]() {
std::cout << x << ", " << y << ", " << z << "\n";
};
this 캡처
멤버 함수 안에서 람다를 쓰고 멤버 변수에 접근하려면 [this]로 현재 객체를 캡처합니다. 람다 안에서 count처럼 멤버를 쓰면 this를 통해 접근하는 것이며, 객체가 소멸한 뒤에 람다가 호출되면 댕글링이 되므로 수명에 주의해야 합니다. C++17 이상에서는 [*this]로 객체 복사 캡처도 가능합니다.
class Counter {
int count = 0;
public:
void increment() {
// this를 캡처
auto lambda = [this]() {
count++; // 멤버 변수 접근
};
lambda();
}
int getCount() const { return count; }
};
캡처 모드 비교표
| 캡처 문법 | 의미 | 수명 안전 | 사용 시점 |
|---|---|---|---|
[] | 아무것도 캡처 안 함 | 항상 안전 | 외부 변수 불필요 |
[=] | 모든 변수 값 복사 | 나중 호출 시 안전 | 비동기·스레드·저장용 콜백 |
[&] | 모든 변수 참조 | 즉시 호출만 안전 | 동기 콜백, 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, 큰 객체 |
초기화 캡처 (C++14)
[이름 = 식]은 람다 전용 변수를 하나 만들어서 식의 결과로 초기화합니다. 복사 비용을 피하려면 [p = std::move(ptr)]처럼 move로 가져올 수 있어서, unique_ptr이나 큰 객체를 람다로 넘길 때 유용합니다. 기존 변수와 다른 이름을 써도 됩니다.
int x = 10;
// 새 변수 생성
auto lambda = [y = x + 5]() {
std::cout << y << "\n"; // 15
};
// move 캡처
auto ptr = std::make_unique<int>(42);
auto lambda2 = [p = std::move(ptr)]() {
std::cout << *p << "\n";
};
mutable과 예외 지정
mutable 람다
값으로 캡처한 변수는 람다 안에서 const로 취급되어 수정할 수 없습니다. mutable을 붙이면 그 복사본은 수정 가능해지지만, 원본 x는 변하지 않습니다. 호출할 때마다 복사본이 갱신되는 것이 아니라, 람다 객체가 들고 있는 캡처 복사본만 바뀝니다.
int x = 0;
// 값 캡처는 기본적으로 const
auto lambda1 = [x]() {
// x++; // ❌ 에러: const
std::cout << x << "\n";
};
// mutable: 값 캡처를 수정 가능
auto lambda2 = [x]() mutable {
x++; // ✅ OK (복사본 수정)
std::cout << x << "\n";
};
lambda2(); // 1
lambda2(); // 2
std::cout << x << "\n"; // 0 (원본은 변하지 않음)
noexcept 지정
noexcept를 붙이면 이 람다가 예외를 던지지 않는다고 선언하는 것입니다. noexcept 함수에서 호출하거나, 이동 시 예외를 막고 싶을 때 사용하며, 실제로 예외를 던지면 std::terminate가 호출됩니다.
auto lambda = []() noexcept {
// 예외를 던지지 않음을 보장
return 42;
};
속성 지정
C++23부터는 람다 도입부 바로 뒤에 속성을 붙여 호출 연산자에 적용할 수 있습니다(P2173). () 뒤에 쓰는 []() [[nodiscard]]는 호출 연산자가 아니라 함수 타입에 붙는 속성이라, nodiscard 경고를 기대했다면 동작하지 않거나 컴파일러가 무시한다는 경고를 냅니다.
// C++23: 속성은 [] 바로 뒤에
auto lambda = [] [[nodiscard]] () {
return 42;
};
lambda(); // ⚠️ 반환값 무시 경고
auto 매개변수와 템플릿 람다
auto 매개변수
C++14부터 람다 매개변수에 auto를 쓸 수 있어서, 호출될 때마다 인자 타입에 맞는 템플릿 인스턴스가 생성됩니다. 한 람다로 여러 타입을 받을 수 있어서, 작은 제네릭 함수를 인라인으로 쓸 때 편합니다.
// 모든 타입 받기
auto print = [](auto value) {
std::cout << value << "\n";
};
print(42); // int
print(3.14); // double
print("hello"); // const char*
여러 타입 매개변수
auto add = [](auto a, auto b) {
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에서는 람다에 템플릿 매개변수를 직접 쓸 수 있습니다. []<typename T>(T value)처럼 하면 타입 T를 본문 안에서 사용할 수 있어서, typeid나 타입별 분기 같은 처리가 필요할 때 유용합니다.
auto lambda = []<typename T>(T value) {
std::cout << typeid(T).name() << ": " << value << "\n";
};
lambda(42); // int: 42
lambda(3.14); // double: 3.14
typeid(T).name()의 출력은 구현마다 다릅니다(GCC는 i, d처럼 맹글링된 이름을 출력). 위 주석은 의미를 보여 주기 위한 것입니다.
제네릭 람다 + if constexpr
auto 매개변수와 if constexpr를 함께 쓰면 타입별로 다른 처리를 한 람다 안에 담을 수 있습니다. 선택되지 않은 분기는 인스턴스화되지 않으므로, 해당 타입에서 컴파일되지 않는 코드가 있어도 괜찮습니다.
auto process = [](auto value) {
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
STL 알고리즘, 스레드, 콜백, RAII 헬퍼에서 람다 쓰기
패턴 1: STL 알고리즘
find_if, count_if, all_of, transform 같은 STL 알고리즘은 조건이나 변환을 predicate/함수로 받습니다. 이런 자리에 람다를 넘기면 “짝수인지”, “제곱” 같은 로직을 호출 지점에 바로 적을 수 있어서, 별도 함수를 만들 필요가 없습니다.
완전한 예제 (복사 후 실행 가능):
// 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};
// find_if: 나이 25 이상 첫 번째
auto it = std::find_if(people.begin(), people.end(),
[](const Person& p) { return p.age >= 25; });
if (it != people.end())
std::cout << "Found: " << it->name << ", " << it->age << "\n";
// count_if: 나이 25 이상 인원 수
int count = std::count_if(people.begin(), people.end(),
[](const Person& p) { return p.age >= 25; });
std::cout << "Count (age>=25): " << count << "\n";
// all_of: 모두 성인(18세 이상)?
bool allAdult = std::all_of(people.begin(), people.end(),
[](const Person& p) { return p.age >= 18; });
std::cout << "All adult: " << (allAdult ? "yes" : "no") << "\n";
// transform: 제곱
std::vector<int> squares;
std::transform(numbers.begin(), numbers.end(),
std::back_inserter(squares),
[](int x) { 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
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수 찾기
auto it = std::find_if(numbers.begin(), numbers.end(),
[](int x) { return x % 2 == 0; });
// 짝수 개수
int count = std::count_if(numbers.begin(), numbers.end(),
[](int x) { return x % 2 == 0; });
// 모두 양수?
bool allPositive = std::all_of(numbers.begin(), numbers.end(),
[](int x) { return x > 0; });
// 변환
std::vector<int> squares;
std::transform(numbers.begin(), numbers.end(),
std::back_inserter(squares),
[](int x) { return x * x; });
패턴 2: 정렬
sort의 세 번째 인자로 비교 함수를 넘기면, “a가 b보다 앞에 오려면” 조건을 람다로 줄 수 있습니다. 나이 비교, 이름 길이 비교처럼 기준을 바꿀 때마다 새 람다만 넣으면 되어서, 함수 객체 클래스를 따로 만들 필요가 없습니다.
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 20}
};
// 나이순 정렬
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
// 이름 길이순
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.name.length() < b.name.length();
});
패턴 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;
// ✅ 값 캡처: data, multiplier 복사본을 스레드가 소유
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: 콜백
버튼 클릭처럼 나중에 호출될 동작을 저장할 때 std::function에 람다를 넘깁니다. [&clickCount]로 참조 캡처하면 바깥 변수를 갱신할 수 있지만, Button이 람다보다 오래 살 때만 안전합니다. 콜백이 객체보다 오래 살 수 있으면 값 캡처나 shared_ptr 등으로 수명을 맞추는 것이 좋습니다.
class Button {
std::function<void()> onClick;
public:
void setOnClick(std::function<void()> callback) {
onClick = callback;
}
void click() {
if (onClick) onClick();
}
};
int main() {
Button button;
int clickCount = 0;
button.setOnClick([&clickCount]() {
clickCount++;
std::cout << "Clicked " << clickCount << " times\n";
});
button.click(); // Clicked 1 times
button.click(); // Clicked 2 times
}
[&clickCount] 대신 [clickCount]() mutable로 값 캡처하면 콜백 안의 카운터는 계속 올라가지만 main의 clickCount는 0으로 남습니다. 콜백이 가진 복사본만 바뀌기 때문입니다. 수명과 상태 공유를 동시에 만족해야 한다면 auto count = std::make_shared<int>(0);을 만들어 [count]로 캡처하거나, 상태를 클래스 멤버로 옮기는 편이 안전합니다.
패턴 4-1: mutable 상태를 가진 람다
초기화 캡처와 mutable을 함께 쓰면 호출될 때마다 상태가 바뀌는 작은 생성기를 클래스 없이 만들 수 있습니다. 상태는 람다 객체 안에 있으므로, 람다를 복사하면 카운터도 따로 복사된다는 점을 기억해 두세요.
auto counter = [n = 0]() mutable {
return ++n;
};
std::cout << counter() << "\n"; // 1
std::cout << counter() << "\n"; // 2
auto copy = counter; // n == 2인 상태로 복사
std::cout << copy() << "\n"; // 3
std::cout << counter() << "\n"; // 3 (원본과 복사본이 독립)
패턴 4-2: [*this]로 객체를 복사해 스레드에 넘기기 (C++17)
[this]는 포인터만 복사하므로 객체가 먼저 소멸하면 댕글링입니다. 스레드가 객체의 스냅샷만 필요하다면 [*this]로 객체 전체를 복사해 넘길 수 있습니다. 객체가 크거나 복사 불가능하면 shared_from_this()를 사용하는 방식(아래 오류 5)을 고려하세요.
struct Worker {
int id = 0;
std::thread runAsync() {
// [*this]: 객체 복사본을 람다가 소유
return std::thread([*this]() {
std::cout << "Worker " << id << " running\n";
});
}
};
int main() {
std::thread t;
{
Worker w;
w.id = 42;
t = w.runAsync();
} // w는 여기서 소멸하지만 스레드는 복사본을 사용
t.join();
}
패턴 5: RAII 헬퍼
스코프를 벗어날 때 반드시 한 번 실행되어야 하는 정리 코드(파일 닫기, 잠금 해제 등)를 람다로 넘겨서 ScopeGuard에 넣습니다. 생성자에서 람다를 받으며, 소멸자에서 실행합니다. dismiss()로 “이미 처리했으니 실행하지 말라”고 표시할 수 있어서, 예외와 정상 경로 모두에서 안전하게 정리할 수 있습니다.
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; }
};
template <typename Func>
auto makeScopeGuard(Func func) {
return ScopeGuard<Func>(std::move(func));
}
void processFile(const std::string& filename) {
FILE* file = fopen(filename.c_str(), "r");
if (!file) return;
// 자동으로 파일 닫기
auto guard = makeScopeGuard([file]() {
fclose(file);
std::cout << "File closed\n";
});
// 파일 처리...
// 스코프 종료 시 자동으로 닫힘
}
패턴 6: 지연 실행
Lazy는 처음 호출될 때만 람다를 실행하며, 그 결과를 캐시해 둡니다. 이후 호출에서는 캐시된 값을 반환하므로, 비용이 큰 계산을 “필요할 때 한 번만” 하게 할 수 있습니다. 람다로 계산 로직을 넘기고, optional로 결과만 보관하는 패턴입니다.
template <typename Func>
class Lazy {
Func func;
mutable std::optional<decltype(func())> cached;
public:
Lazy(Func f) : func(std::move(f)) {}
auto operator()() const {
if (!cached) {
cached = func();
}
return *cached;
}
};
int main() {
Lazy expensive( []() {
std::cout << "Computing...\n";
return 42;
});
std::cout << expensive() << "\n"; // Computing... 42
std::cout << expensive() << "\n"; // 42 (캐시됨)
}
패턴 7: 재귀 람다
람다가 자기 자신을 호출하려면 이름이 필요한데, 람다는 기본적으로 이름이 없습니다. C++14에서는 std::function에 담고 참조 캡처 [&]로 그 function을 캡처하면, 본문에서 자기 자신을 호출할 수 있습니다. C++23에서는 this auto self로 재귀 호출 대상을 명시할 수 있습니다.
// C++14: std::function 사용
std::function<int(int)> factorial = [&](int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
};
std::cout << factorial(5) << "\n"; // 120
// C++23: 명시적 this
auto factorial2 = [](this auto self, int n) {
return n <= 1 ? 1 : n * self(n - 1);
};
주의: std::function 방식에서는 factorial을 반드시 참조로 캡처해야 합니다. [=]나 [factorial]로 값 캡처하면 캡처 시점의 factorial은 아직 람다가 대입되기 전의 빈 std::function이라, 재귀 호출 시 std::bad_function_call 예외가 발생합니다. 또 참조로 캡처한 이 람다를 다른 곳에 복사해 반환하면, 원래 factorial 변수가 사라진 뒤 댕글링이 됩니다. 재귀 로직이 길어지면 이름 있는 일반 함수로 빼는 편이 읽기에도 디버깅에도 낫습니다. C++23 이전의 다른 우회법과 deducing this 비교는 아래 C++17/20/23 최신 기능: constexpr·consteval·재귀 람다 절에서 다룹니다.
댕글링 참조, 루프 변수 캡처, this 수명 오류
오류 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++; // 복사본만 수정, 원본 x는 그대로
};
오류 4: std::function과 람다 타입 불일치
증상: std::function에 람다를 담을 때 캡처가 있으면 타입이 달라서, 템플릿이 아닌 고정 시그니처 함수에 넘기기 어렵습니다.
해결: std::function<void()>는 캡처 있는 람다도 받을 수 있습니다. 다만 std::function은 힙 할당을 하므로, 성능이 중요하면 template<typename F>로 람다를 그대로 전달하는 편이 좋습니다.
// ✅ std::function은 캡처 람다도 OK
std::function<void()> f = [x = 42]() { std::cout << x << "\n"; };
// ✅ 성능 중요 시: 템플릿으로 람다 직접 전달
template <typename Func>
void run(Func&& f) { f(); }
run( []() { std::cout << "no heap\n"; });
오류 5: 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();
}
};
shared_from_this()는 객체가 이미 std::shared_ptr로 관리되고 있을 때만 쓸 수 있습니다. 스택에 만든 Worker에서 호출하면 C++17부터 std::bad_weak_ptr 예외가 발생합니다.
오류 6: std::function 시그니처 불일치
증상: conversion from lambda to non-scalar type std::function<int()> 같은 긴 컴파일 에러.
원인: std::function의 반환·인자 타입으로 람다를 호출할 수 없을 때입니다. 반환 타입이 void인 람다를 std::function<int()>에 담으려 하면 변환할 방법이 없습니다.
// ❌ std::function<int()> fn = []() { std::cout << "hi"; }; // 람다는 void 반환
// ✅
std::function<void()> fn = []() { std::cout << "hi\n"; };
오류 7: 초기화 캡처가 다른 캡처를 참조한다고 착각
증상: 'y' was not declared in this scope 컴파일 에러 또는 예상과 다른 값.
원인: 초기화 캡처 [a = 식]의 식은 람다를 감싸는 바깥 스코프에서 평가됩니다. 같은 캡처 리스트의 다른 초기화 캡처를 가리킬 수 없습니다.
int x = 10;
// ❌ y는 바깥 스코프에 없으므로 에러 (같은 리스트의 y = x를 가리키지 않음)
// auto bad = [z = y, y = x]() { return z + y; };
// ✅ 바깥 변수만 사용
auto good = [y = x, z = x * 2]() { return y + z; };
오류 8: mutable 람다를 여러 스레드에서 공유
증상: 데이터 레이스, 실행할 때마다 다른 결과.
원인: 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::atomic<int>를 참조로)
std::thread t3([counter]() mutable { for (int i = 0; i < 100; ++i) counter(); });
오류 요약
| 오류 | 원인 | 해결법 |
|---|---|---|
| 댕글링 참조 | 참조 캡처 후 스코프 종료 | 값 캡처 [x] |
| for 루프 i | [&i] 참조 캡처 | [i] 값 캡처 |
| 값 캡처 수정 | 호출 연산자가 const | mutable 추가 |
| this 소멸 | 객체가 먼저 소멸 | shared_from_this, [*this] |
| std::function 타입 | 시그니처 불일치 | 반환/인자 타입 일치 |
| 초기화 캡처 | 다른 캡처를 참조 | 바깥 변수로만 초기화 |
| mutable + 멀티스레드 | 공유 람다 객체 수정 | 스레드마다 복사 또는 atomic |
캡처 비용 줄이기와 std::function 대신 템플릿
팁 1: 캡처 비용 최소화
- 작은 타입(int, 포인터): 값 캡처
[x]로 복사 비용 무시 가능 - 큰 객체(std::string, vector): 참조
[&s]또는 move[s = std::move(str)] - 여러 변수: 필요한 것만
[a, &b]로 선택 캡처.[=]는 사용하지 않는 변수까지 복사할 수 있음
std::string big(10000, 'x');
int threshold = 10;
// ❌ big 전체 복사
auto bad = [=]() { return big.size() > threshold; };
// ✅ 참조 + 값
auto good = [&big, threshold]() { return big.size() > threshold; };
팁 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);
}
| 방식 | 인라인 가능 | 힙 할당 | 호출 비용 | 사용 시점 |
|---|---|---|---|---|
| 람다 (템플릿으로 전달) | ✅ | ❌ | 직접 호출 | 콜백을 받아 바로 쓸 때 |
| 람다 (std::function에 저장) | 대체로 ❌ | 큰 클로저면 가능 | 간접 호출 | 서로 다른 콜백을 한 타입으로 저장할 때 |
| 함수 포인터 | 상황에 따라 | ❌ | 간접 호출 | C API 연동 (캡처 없는 람다는 함수 포인터로 변환 가능) |
| 함수 객체 | ✅ | ❌ | 직접 호출 | 상태와 여러 멤버 함수가 필요할 때 |
“std::function은 느리다”는 말은 절반만 맞습니다. 호출 한 번의 간접 비용보다 문제가 되는 건 인라인이 막힌다는 점입니다. std::sort처럼 비교 함수를 수백만 번 호출하는 루프에서는 비교 로직이 루프 안으로 인라인되느냐가 차이를 만들고, 이벤트 핸들러처럼 가끔 호출되는 콜백에서는 거의 체감되지 않습니다. 그래서 “저장해야 하면 std::function, 받아서 바로 쓰면 템플릿”이 실용적인 기준입니다.
팁 3: noexcept 람다
예외를 던지지 않는 람다에 noexcept를 붙이면, 이동 연산 등에서 컴파일러가 더 나은 코드를 생성할 수 있습니다.
auto safe = []() noexcept { return 42; };
팁 4: 즉시 실행 람다로 복잡 초기화
복잡한 초기화를 한 번만 수행하며, 결과만 변수에 넣을 때 즉시 실행 람다를 쓰면 중간 임시 객체를 줄일 수 있습니다.
// ✅ 한 번만 계산, 이동으로 반환
auto config = [&]() {
Config c;
c.loadFromFile("config.json");
c.merge(defaults);
return c; // RVO 또는 이동
}();
std::async, ScopeGuard, 재시도 핸들러 주입
패턴 A: 스레드에 안전하게 인자 전달
스레드에 넘기는 람다는 반드시 값 캡처로 필요한 데이터를 복사합니다. 참조 캡처는 메인 스레드가 먼저 진행해 변수를 소멸시킬 수 있어 위험합니다.
void processInBackground(const std::string& input) {
// ✅ 값 캡처: input 복사본을 스레드가 소유
std::thread t([input]() {
auto result = expensiveComputation(input);
saveResult(result);
});
t.detach(); // 또는 join()으로 대기
}
패턴 B: std::async와 람다
std::async에 람다를 넘길 때도 값 캡처를 사용합니다. future가 나중에 get()될 때까지 지역 변수는 살아 있어야 하므로, 값 캡처가 안전합니다.
auto future = std::async(std::launch::async, [data = prepareData()]() {
return process(data);
});
// ... 다른 작업 ...
auto result = future.get();
패턴 C: 에러 처리와 함께하는 ScopeGuard
프로덕션에서는 예외가 발생해도 리소스를 안전하게 해제해야 합니다. ScopeGuard에 람다로 정리 로직을 넘기고, dismiss()로 성공 시 중복 실행을 막습니다.
#include <fstream>
#include <filesystem>
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 = makeScopeGuard([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: 재시도 로직에 에러 핸들러 주입
재시도·로깅 정책을 람다로 주입하면 같은 재시도 뼈대를 여러 작업에 재사용할 수 있습니다. 작업 람다는 즉시 호출되므로 [&] 캡처도 안전합니다.
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");
}
auto body = tryWithRetry(
[&]() { return fetch(url); },
[](int attempt, const char* msg) { std::cerr << "retry " << attempt << ": " << msg << "\n"; });
함수 밖으로 나가는 람다의 캡처 주의점
댕글링 참조
지역 변수 x를 참조로 캡처한 람다를 반환하면, 함수가 끝난 뒤에는 x가 이미 소멸해 댕글링 참조가 됩니다. 그 람다를 나중에 호출하면 미정의 동작입니다. 따라서 함수 밖으로 나가는 람다에서는 참조 캡처 대신 값 캡처로 필요한 값만 복사해 두는 것이 안전합니다.
std::function<void()> createLambda() {
int x = 42;
// ❌ 위험: x의 참조 캡처
return [&x]() {
std::cout << x << "\n"; // x는 이미 소멸!
};
}
// ✅ 안전: 값 캡처
std::function<void()> createLambda() {
int x = 42;
return [x]() {
std::cout << x << "\n";
};
}
캡처 비용
[largeString]처럼 값 캡처하면 그 순간 복사가 일어납니다. 크기가 큰 객체면 비용이 들고, 람다가 한 번만 호출되거나 읽기만 할 때는 참조 캡처 [&largeString]로 복사를 피할 수 있습니다. 람다로 옮긴 뒤 원본을 더 쓰지 않을 거라면 초기화 캡처 [s = std::move(largeString)]로 이동하는 것이 좋습니다.
std::string largeString(10000, 'x');
// ❌ 복사 비용
auto lambda1 = [largeString]() {
// largeString 복사됨
};
// ✅ 참조 사용
auto lambda2 = [&largeString]() {
// 참조만
};
// ✅ move 캡처
auto lambda3 = [s = std::move(largeString)]() {
// move됨
};
C++17/20/23 최신 기능: constexpr·consteval·재귀 람다
C++17: 암묵적 constexpr 람다
C++17부터 람다의 호출 연산자는 constexpr 함수의 요건을 만족하면 암묵적으로 constexpr입니다. 상수 표현식이 필요한 문맥에서 호출하면 컴파일 타임에 평가됩니다.
// C++17+ : 자동으로 constexpr
constexpr auto factorial = [](int n) {
int result = 1;
for (int i = 2; i <= n; ++i)
result *= i;
return result;
};
// 컴파일 타임 계산 (template argument, constexpr 변수)
constexpr int val = factorial(5); // 120, 컴파일 타임에 계산
std::array<int, factorial(4)> arr; // 크기 24 배열
// 런타임에도 사용 가능
int n = read_input();
int runtime_val = factorial(n); // 런타임 계산
실무 활용: 컴파일 타임 계산으로 런타임 비용 제거, LUT(Lookup Table) 생성 등.
// 컴파일 타임 LUT 생성 (C++17: std::array의 operator[]가 constexpr)
constexpr auto generate_lut = []() {
std::array<int, 256> lut{};
for (int i = 0; i < 256; ++i)
lut[i] = (i * i) % 256; // 임의의 계산
return lut;
};
constexpr auto lookup_table = generate_lut(); // 컴파일 타임 생성
// 런타임에서 O(1) 조회
int fast_lookup(int x) {
return lookup_table[x & 0xFF];
}
C++20: consteval 람다 (반드시 컴파일 타임)
consteval 람다는 반드시 컴파일 타임에만 실행됩니다. 런타임 인자로 호출하면 컴파일 에러. consteval은 변수 선언 앞이 아니라 람다의 매개변수 목록 뒤에 지정자로 붙입니다(consteval auto f = [...]처럼 변수에 붙이면 문법 오류).
// C++20 consteval: 컴파일 타임 전용
auto square = [](int n) consteval {
return n * n;
};
constexpr int a = square(5); // ✅ OK: 컴파일 타임
// int b = square(runtime_value); // ❌ 컴파일 에러!
// 용도: 컴파일 타임 검증
auto validate_config = [](const char* str) consteval {
if (str[0] == '\0')
throw "Invalid config"; // 이 분기에 도달하면 상수 평가 실패 → 컴파일 에러
return true;
};
constexpr bool ok = validate_config("prod"); // 컴파일 타임 검증
throw를 컴파일 타임 검증에 쓰는 것은 흔한 관용구입니다. 상수 평가 중에 throw에 도달하면 그 식은 상수 표현식이 아니게 되고, consteval 호출이므로 곧바로 컴파일 에러로 드러납니다.
컴파일 타임 문자열 해싱:
// case 레이블에 쓸 해시를 컴파일 타임에 계산
auto compile_time_hash = [](std::string_view sv) consteval {
std::size_t hash = 0;
for (char c : sv)
hash = hash * 31 + static_cast<unsigned char>(c);
return hash;
};
switch (runtime_event_id) {
case compile_time_hash("user_login"): // 컴파일 타임 계산
handle_login();
break;
case compile_time_hash("user_logout"):
handle_logout();
break;
}
C++23: Deducing This로 재귀 람다 해결
역사적 문제: C++20까지 람다 재귀는 복잡한 workaround가 필요했습니다.
// ❌ C++20: 재귀 불가능 (람다는 자기 이름이 없음)
auto factorial = [](int n) {
return n <= 1 ? 1 : n * factorial(n - 1); // 컴파일 에러!
};
// ❌ Workaround 1: std::function (성능 overhead)
std::function<int(int)> factorial = [&](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
// ❌ Workaround 2: Y-combinator (복잡함)
auto Y = [](auto f) {
return [=](auto... args) {
return f(f, args...);
};
};
auto factorial = Y([](auto self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
});
✅ C++23 해결: Deducing This
// ✅ C++23: 깔끔한 재귀 람다
auto factorial = [](this auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
};
std::cout << factorial(5); // 120
// 실전 예제: 트리 순회
struct Node {
int value;
std::vector<Node*> children;
};
auto traverse = [](this auto&& self, const Node* node, int depth) -> void {
std::cout << std::string(depth * 2, ' ') << node->value << '\n';
for (const auto* child : node->children)
self(child, depth + 1); // 재귀 호출!
};
Node root{1, {new Node{2, {}}, new Node{3, {}}}};
traverse(&root, 0);
트리 예제의 new Node는 설명을 줄이기 위한 것이라 해제하지 않습니다. 실제 코드에서는 std::vector<std::unique_ptr<Node>> 같은 소유 구조를 쓰세요.
| 방법 | 호출 비용 | 코드 복잡도 | C++ 버전 |
|---|---|---|---|
| std::function | 간접 호출, 인라인 어려움 | 간단 | C++11+ |
| Y-combinator / self 인자 전달 | 인라인 가능 | 호출부가 번거로움 | C++14+ |
| Deducing this | 인라인 가능 | 간단 | C++23 |
선택 기준:
- C++23을 쓸 수 있으면 →
deducing this - C++20 이하 → 호출 빈도가 높으면 Y-combinator 방식, 가독성이 우선이면 std::function
가장 단순한 Y-combinator 형태는 람다가 자기 자신을 첫 인자로 받는 방식입니다. 호출할 때마다 factorial(factorial, 5)처럼 자기 자신을 넘겨야 해서 번거롭지만, std::function 없이 동작합니다.
auto factorial = [](auto self, int n) -> int {
if (n <= 1) return 1;
return n * self(self, n - 1);
};
std::cout << factorial(factorial, 5) << "\n"; // 120
// 파일 시스템 재귀 탐색
namespace fs = std::filesystem;
auto find_files = [](this auto&& self, const fs::path& dir,
const std::string& ext) -> std::vector<fs::path> {
std::vector<fs::path> results;
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_directory()) {
auto sub = self(entry.path(), ext); // 재귀!
results.insert(results.end(), sub.begin(), sub.end());
} else if (entry.path().extension() == ext) {
results.push_back(entry.path());
}
}
return results;
};
auto cpp_files = find_files("/project/src", ".cpp");
같이 보면 좋은 글
- C++ STL 알고리즘 기초: sort·find·transform·accumulate 실전 활용
- C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
- C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
- C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴
- C++ 람다 캡처 에러 해결
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
- C++ vector 기초 | 초기화·연산·용량 관리와 실전 패턴
- C++ std::function | 콜백·전략 패턴과 함수 객체
- C++ map·unordered_map·flat_map: 성능 비교와 선택 기준
- C++ 컨테이너 선택 가이드 | vector/list/deque/map/set 상황별 선택과 성능 최적화
자주 묻는 질문 (FAQ)
Q. 값으로 캡처한 변수를 람다 안에서 수정하면 컴파일 에러가 나는 이유는 무엇인가요?
A. 람다가 만드는 클로저의 operator()는 기본적으로 const라서 값 캡처한 복사본을 바꿀 수 없습니다. mutable을 붙이면 수정은 가능하지만 바뀌는 것은 람다 내부의 복사본이지 바깥 원본 변수가 아닙니다. 원본을 바꿔야 한다면 참조 캡처를 쓰되, 람다가 변수보다 오래 살아남아 댕글링 참조가 되지 않는지 확인해야 합니다.