C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴
이 글의 핵심
C++ 람다 심화에 대한 실전 가이드입니다. 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴 등을 예제와 함께 상세히 설명합니다.
들어가며: unique_ptr을 스레드에 넘기려면 어떻게 해야 할까요?
”소유권을 넘기고 싶은데 [=]로는 복사가 안 되고, [&]로는 댕글링이…”
비동기 작업 큐에 unique_ptr을 담은 작업을 넣고 싶었습니다. std::unique_ptr은 복사가 불가능하고 이동만 가능합니다. [=]로 캡처하면 복사 시도로 컴파일 에러가 나고, [&]로 캡처하면 지역 변수가 스코프를 벗어난 뒤 큐에서 작업이 실행될 때 댕글링 참조가 됩니다.
자주 겪는 문제 시나리오들
시나리오 1: unique_ptr을 람다로 넘기기
std::thread나 std::async에 unique_ptr을 담은 작업을 넘기려면, 소유권 이전이 필요합니다. [=]는 복사라서 불가능하고, [&]는 수명 문제가 있습니다. 초기화 캡처 [p = std::move(ptr)]로 람다 전용 변수에 이동하면 해결됩니다.
시나리오 2: 제네릭 람다에서 인자를 그대로 전달하기
래퍼 함수가 인자를 받아서 내부 함수에 그대로 넘겨야 할 때, lvalue는 lvalue로, rvalue는 rvalue로 전달해야 불필요한 복사가 없습니다. std::forward와 완벽 전달을 람다에 적용하는 패턴이 필요합니다.
시나리오 3: const 변수 초기화를 조건에 따라 다르게
const 변수를 if-else 분기마다 다른 값으로 초기화하려면, 블록을 나눠야 해서 스코프가 복잡해집니다. IIFE(즉시 실행 함수 표현식)로 한 블록 안에서 초기화를 완료할 수 있습니다.
시나리오 4: 재귀를 람다로 쓰고 싶을 때
std::function에 담긴 람다는 자기 자신을 호출할 수 없어서, 재귀 로직을 람다로 쓰기 어렵습니다. std::function + 재귀 람다 패턴이나 Y combinator를 쓰면 람다만으로 재귀를 표현할 수 있습니다.
시나리오 5: 호출마다 상태를 유지하는 함수가 필요할 때
카운터, 시퀀스 생성기, 캐시처럼 호출 사이에 상태를 유지하는 함수가 필요합니다. 전역 변수는 스레드 안전하지 않고, 별도 클래스를 만들기는 과합니다. 상태 보유 람다(stateful lambda)는 mutable과 초기화 캡처 [n = 0]로 간단히 구현할 수 있습니다.
시나리오 6: 템플릿 함수에 람다를 넘길 때
std::sort, std::find_if처럼 템플릿으로 콜백을 받는 함수에 람다를 넘기면, 컴파일러가 인라인 최적화를 수행해 std::function보다 빠릅니다. 반대로 std::function으로 타입을 고정하면 타입 소거로 인라인이 어렵습니다. 템플릿 내 람다 사용 시점을 이해하면 성능과 유연성을 맞출 수 있습니다.
시나리오 7: 정렬·검색 기준을 동적으로 바꿀 때
std::sort, std::set, std::priority_queue에 비교자(comparator)를 넘겨야 합니다. 나이순, 이름순, 점수순처럼 기준이 바뀔 때마다 람다로 비교자를 넘기면, 호출 지점에서 의도가 분명해집니다. 람다 비교자는 operator<를 오버로드한 함수 객체보다 간결합니다.
시나리오 8: 콜백을 저장해 나중에 호출할 때
버튼 클릭, 이벤트 핸들러처럼 나중에 호출될 함수를 저장하려면 std::function에 람다를 담습니다. 이때 람다가 캡처한 변수의 수명이 std::function보다 길어야 댕글링이 발생하지 않습니다. std::function + 람다 조합에서 수명 관리가 핵심입니다.
정의를 풀어 쓰면 초기화 캡처는 람다가 생성될 때 새 변수를 만들어 식의 결과로 초기화하는 문법입니다. 비유하면 람다 “입장권”을 받을 때 필요한 짐을 한 번만 싸서 들고 들어가는 것입니다. unique_ptr 이동, 큰 객체의 move, 표현식 결과 캡처에 쓰입니다.
문제의 코드:
// ❌ unique_ptr은 복사 불가
void enqueueTask() {
auto ptr = std::make_unique<Payload>(...);
taskQueue.push([=]() { // 컴파일 에러!
process(ptr);
});
}
// ❌ 참조 캡처는 enqueueTask() 종료 후 댕글링
taskQueue.push([&ptr]() { process(ptr); });
초기화 캡처로 해결:
// ✅ 소유권 이전
taskQueue.push([p = std::move(ptr)]() {
process(std::move(p));
});
이 글을 읽으면:
- 초기화 캡처로 unique_ptr·큰 객체를 안전하게 넘기는 방법을 알 수 있습니다.
- 완벽 전달을 람다에 적용하는 패턴을 배울 수 있습니다.
- IIFE로 복잡한 const 초기화를 단순화할 수 있습니다.
- 재귀 람다를 구현하는 방법을 익힐 수 있습니다.
- 자주 겪는 에러와 프로덕션 패턴을 정리할 수 있습니다.
람다 고급 캡처와 전달 흐름을 요약하면 아래와 같습니다.
flowchart TB
subgraph init["초기화 캡처"]
A[람다 정의] --> B["p = std move ptr"]
B --> C[람다 내부 p 소유]
end
subgraph iife["IIFE"]
D[람다 정의] --> E[즉시 호출]
E --> F[결과만 변수에 저장]
end
subgraph recurse["재귀 람다"]
G["std function 저장"] --> H[람다 내부에서 self 호출]
end
목차
- 초기화 캡처 (Init Capture)
- 완벽 전달 (Perfect Forwarding)
- IIFE (즉시 실행 함수 표현식)
- 재귀 람다 (Recursive Lambda)
- 완전한 람다 고급 예제 모음 (상태 보유·템플릿·비교자·std::function)
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
1. 초기화 캡처 (Init Capture)
기본 문법
C++14부터 [이름 = 식] 형태로 람다 전용 변수를 만들 수 있습니다. 이 변수는 람다가 정의될 때 한 번만 초기화되고, 람다 본문에서 사용합니다.
// [변수명 = 초기화식]
auto lambda = [value = 42]() {
return value * 2; // 84
};
unique_ptr 이동 캡처
unique_ptr은 복사가 불가능하므로, 초기화 캡처로 이동해서 람다에 넘깁니다.
// g++ -std=c++17 -o init_capture_unique init_capture_unique.cpp && ./init_capture_unique
#include <iostream>
#include <memory>
struct Payload {
int id;
Payload(int i) : id(i) {}
};
void process(std::unique_ptr<Payload> p) {
if (p) std::cout << "Processing id=" << p->id << "\n";
}
int main() {
auto ptr = std::make_unique<Payload>(100);
// [p = std::move(ptr)]: ptr의 소유권을 람다 내부 p로 이전
auto task = [p = std::move(ptr)]() {
process(std::move(p));
};
task(); // Processing id=100
return 0;
}
실행 결과:
Processing id=100
위 코드 설명: [p = std::move(ptr)]에서 ptr은 빈 상태가 되고, 람다 객체 내부에 p라는 unique_ptr<Payload> 멤버가 생성됩니다. 람다가 실행될 때 process(std::move(p))로 한 번 더 이동해 전달합니다.
큰 객체 move 캡처
std::string, std::vector처럼 큰 객체를 람다에 넘길 때, 복사 대신 이동으로 전달하면 성능이 좋아집니다.
#include <string>
#include <vector>
int main() {
std::string bigString(10000, 'x');
std::vector<int> bigVec(1000, 42);
// 복사 대신 이동으로 캡처
auto lambda = [s = std::move(bigString), v = std::move(bigVec)]() {
std::cout << "s.size()=" << s.size() << ", v.size()=" << v.size() << "\n";
};
lambda();
// bigString, bigVec은 이제 빈 상태 (이동됨)
return 0;
}
표현식 결과 캡처
캡처 시점에 계산된 값을 람다 전용 변수에 넣을 수 있습니다.
int x = 10;
int y = 20;
// [sum = x + y]: 정의 시점에 30이 계산되어 sum에 저장
auto lambda = [sum = x + y]() {
return sum; // 30
};
x = 100; // sum에는 영향 없음
std::cout << lambda() << "\n"; // 30
초기화 캡처와 기존 캡처 혼합
[=, p = std::move(ptr)]처럼 기존 캡처와 함께 쓸 수 있습니다.
int threshold = 10;
auto ptr = std::make_unique<int>(42);
auto lambda = [=, p = std::move(ptr)]() {
if (p && *p > threshold) {
return true;
}
return false;
};
초기화 캡처 비교표
| 문법 | 의미 | 사용 시점 |
|---|---|---|
[x = expr] | expr 결과를 x에 저장 | 표현식 결과 캡처 |
[p = std::move(ptr)] | ptr 소유권 이전 | unique_ptr, 큰 객체 |
[s = std::move(str)] | str 이동 | string, vector |
[c = getConnection()] | 연결 객체 캡처 | 리소스 획득 |
2. 완벽 전달 (Perfect Forwarding)
왜 완벽 전달이 필요한가
제네릭 람다나 래퍼에서 인자를 내부 함수에 넘길 때, lvalue는 lvalue로, rvalue는 rvalue로 전달해야 불필요한 복사를 막을 수 있습니다. std::forward를 사용합니다.
기본 패턴
// g++ -std=c++17 -o perfect_forward_lambda perfect_forward_lambda.cpp && ./perfect_forward_lambda
#include <iostream>
#include <utility>
template <typename Func, typename... Args>
decltype(auto) wrapper(Func&& f, Args&&... args) {
// f에 args를 완벽 전달
return std::forward<Func>(f)(std::forward<Args>(args)...);
}
int main() {
auto add = { return a + b; };
std::cout << wrapper(add, 3, 5) << "\n"; // 8
auto identity = -> decltype(auto) {
return std::forward<decltype(x)>(x);
};
int i = 42;
std::cout << identity(i) << "\n"; // 42 (lvalue 그대로)
std::cout << identity(100) << "\n"; // 100 (rvalue 그대로)
return 0;
}
실행 결과:
8
42
100
제네릭 람다에서 완벽 전달
auto&&와 std::forward를 조합하면, 람다가 받은 인자를 그대로 다음 함수에 넘길 수 있습니다.
template <typename F>
auto makeLogger(F&& f) {
return [f = std::forward<F>(f)](auto&&... args) -> decltype(auto) {
std::cout << "Calling with " << sizeof...(args) << " args\n";
return f(std::forward<decltype(args)>(args)...);
};
}
auto add = { return a + b; };
auto loggedAdd = makeLogger(add);
std::cout << loggedAdd(1, 2) << "\n"; // Calling with 2 args\n 3
emplace와 완벽 전달
std::vector::emplace_back처럼 생성 인자를 직접 전달할 때 유용합니다.
#include <vector>
#include <string>
template <typename T, typename... Args>
void emplaceAndLog(std::vector<T>& vec, Args&&... args) {
auto doEmplace = [&vec](Args&&... a) {
vec.emplace_back(std::forward<Args>(a)...);
};
doEmplace(std::forward<Args>(args)...);
}
int main() {
std::vector<std::string> v;
emplaceAndLog(v, "hello"); // const char* → string 변환
emplaceAndLog(v, 5, 'x'); // string(5, 'x')
std::cout << v[0] << " " << v[1] << "\n"; // hello xxxxx
return 0;
}
3. IIFE (즉시 실행 함수 표현식)
개념
IIFE(Immediately Invoked Function Expression)는 람다를 정의한 직후 ()를 붙여 바로 호출하는 패턴입니다. “한 번만 쓰는 함수”를 인라인으로 실행해, 결과만 변수에 저장합니다.
기본 형태
// { 본문 }(실제인자)
auto result = { return x * x; }(5); // result = 25
const 변수 조건부 초기화
const 변수를 if-else에 따라 다르게 초기화할 때, IIFE로 한 블록 안에서 처리할 수 있습니다.
// ❌ 블록을 나눠야 함
const std::string config;
if (useFile) {
config = loadFromFile("config.json");
} else {
config = getDefaultConfig(); // 에러: const는 한 번만 초기화
}
// ✅ IIFE로 한 블록에서 초기화
const auto config = [&]() {
if (useFile) {
return loadFromFile("config.json");
} else {
return getDefaultConfig();
}
}();
복잡한 초기화 로직
여러 단계를 거치는 초기화를 IIFE로 캡슐화합니다.
// g++ -std=c++17 -o iife_init iife_init.cpp && ./iife_init
#include <iostream>
#include <string>
#include <vector>
std::string loadFromFile(const std::string& path) {
return "loaded:" + path;
}
std::string getDefault() { return "default"; }
int main() {
bool useFile = true;
const auto config = [&]() -> std::string {
if (useFile) {
auto raw = loadFromFile("config.json");
return raw + "_parsed";
} else {
return getDefault();
}
}();
std::cout << config << "\n"; // loaded:config.json_parsed
return 0;
}
실행 결과:
loaded:config.json_parsed
스코프 제한
IIFE 안에서 만든 변수는 람다 스코프에 갇혀 바깥에 노출되지 않습니다.
const auto result = [&]() {
std::vector<int> temp = {1, 2, 3, 4, 5};
int sum = 0;
for (int x : temp) sum += x;
return sum; // temp는 여기서 소멸
}();
// temp에 접근 불가 → 깔끔한 스코프
lock_guard와 IIFE
락을 잡은 상태에서만 초기화하고, 락 해제 후 사용할 때 유용합니다.
#include <mutex>
std::mutex mtx;
SomeResource* resource = nullptr;
const auto initialized = [&]() {
std::lock_guard<std::mutex> lock(mtx);
if (!resource) {
resource = new SomeResource();
}
return resource;
}();
// lock은 여기서 해제됨
4. 재귀 람다 (Recursive Lambda)
문제: 람다는 자기 자신을 알 수 없다
일반 함수는 이름이 있어서 재귀 호출이 쉽지만, 람다는 이름이 없어서 자기 자신을 호출할 수 없습니다.
// ❌ 람다 안에서 factorial을 아직 정의하지 않음
auto factorial = {
if (n <= 1) return 1;
return n * factorial(n - 1); // 에러: factorial 캡처 안 됨
};
해결 1: std::function에 담기
std::function에 담으면, 재귀 호출 시 그 std::function을 캡처해서 사용할 수 있습니다.
// g++ -std=c++17 -o recursive_lambda recursive_lambda.cpp && ./recursive_lambda
#include <iostream>
#include <functional>
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;
}
실행 결과:
120
위 코드 설명: factorial을 참조 캡처 [&factorial]로 가져와서, 람다 안에서 factorial(n-1)로 자기 자신을 호출합니다. std::function은 타입 소거로 인해 힙 할당이 발생할 수 있어, 성능이 중요한 경로에서는 주의가 필요합니다.
해결 2: Y combinator (고급)
Y combinator를 쓰면 std::function 없이 순수 람다만으로 재귀를 구현할 수 있습니다.
// g++ -std=c++17 -o y_combinator y_combinator.cpp && ./y_combinator
#include <iostream>
template <typename F>
struct Y {
F f;
Y(F f) : f(f) {}
template <typename... Args>
decltype(auto) operator()(Args&&... args) {
return f(*this, std::forward<Args>(args)...);
}
};
template <typename F>
Y(F) -> Y<F>;
int main() {
auto factorial = Y( -> int {
if (n <= 1) return 1;
return n * self(n - 1);
});
std::cout << factorial(5) << "\n"; // 120
return 0;
}
해결 3: C++23 deducing this (자기 참조)
C++23에서는 this를 값으로 받는 explicit object parameter로 재귀를 더 간단히 쓸 수 있습니다.
// C++23
auto factorial = -> int {
if (n <= 1) return 1;
return n * self(n - 1);
};
재귀 람다 비교표
| 방식 | 장점 | 단점 |
|---|---|---|
| std::function | 구현 간단 | 힙 할당 가능, 인라인 어려움 |
| Y combinator | 순수 람다, 인라인 가능 | 문법 복잡 |
| C++23 this | 직관적 | C++23 필요 |
5. 완전한 람다 고급 예제 모음
예제 1: 비동기 작업 큐에 unique_ptr 넘기기
// g++ -std=c++17 -pthread -o async_unique async_unique.cpp && ./async_unique
#include <iostream>
#include <memory>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
struct Task {
int id;
Task(int i) : id(i) {}
};
class TaskQueue {
std::queue<std::function<void()>> queue_;
std::mutex mtx_;
std::condition_variable cv_;
bool done_ = false;
public:
void push(std::function<void()> f) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(f));
cv_.notify_one();
}
void run() {
while (true) {
std::function<void()> f;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return !queue_.empty() || done_; });
if (done_ && queue_.empty()) break;
if (!queue_.empty()) {
f = std::move(queue_.front());
queue_.pop();
}
}
if (f) f();
}
}
void stop() {
std::lock_guard<std::mutex> lock(mtx_);
done_ = true;
cv_.notify_all();
}
};
int main() {
TaskQueue q;
std::thread worker([&q] { q.run(); });
for (int i = 0; i < 3; ++i) {
auto task = std::make_unique<Task>(i);
q.push([p = std::move(task)]() {
std::cout << "Processing task " << p->id << "\n";
});
}
q.stop();
worker.join();
return 0;
}
실행 결과:
Processing task 0
Processing task 1
Processing task 2
예제 2: 완벽 전달 로거 래퍼
#include <iostream>
#include <utility>
#include <string>
template <typename F>
auto makeLogged(F&& f) {
return [f = std::forward<F>(f)](auto&&... args) -> decltype(auto) {
std::cout << ">> call\n";
auto result = f(std::forward<decltype(args)>(args)...);
std::cout << "<< return\n";
return result;
};
}
int main() {
auto add = { return a + b; };
auto concat = { return a + b; };
auto loggedAdd = makeLogged(add);
auto loggedConcat = makeLogged(concat);
std::cout << loggedAdd(1, 2) << "\n";
std::cout << loggedConcat(std::string("Hello"), std::string(" World")) << "\n";
return 0;
}
예제 3: IIFE로 설정 로드
#include <iostream>
#include <string>
#include <map>
std::map<std::string, std::string> loadFromEnv() {
return {{"host", "localhost"}, {"port", "8080"}};
}
std::map<std::string, std::string> loadFromFile(const std::string& path) {
return {{"host", "file_host"}, {"port", "9000"}};
}
int main() {
bool useEnv = true;
const auto config = [&]() -> std::map<std::string, std::string> {
auto raw = useEnv ? loadFromEnv() : loadFromFile("config.json");
raw["version"] = "1.0";
return raw;
}();
std::cout << "host=" << config.at("host") << ", port=" << config.at("port") << "\n";
return 0;
}
예제 4: 재귀 람다로 트리 순회
#include <iostream>
#include <functional>
#include <vector>
struct Node {
int value;
std::vector<Node*> children;
};
void traverse(Node* node, std::function<void(int)>& visit) {
if (!node) return;
visit(node->value);
for (auto* child : node->children) {
traverse(child, visit);
}
}
int main() {
Node n1{1, {}}, n2{2, {}}, n3{3, {}};
Node root{0, {&n1, &n2, &n3}};
std::function<void(int)> print = [&print](int v) {
std::cout << v << " ";
};
traverse(&root, print); // 0 1 2 3
std::cout << "\n";
return 0;
}
6. 자주 발생하는 에러와 해결법
에러 1: unique_ptr을 [=]로 캡처
증상: error: use of deleted function 'std::unique_ptr<...>::unique_ptr(const std::unique_ptr<...>&)'
원인: unique_ptr은 복사 생성자가 삭제되어 있습니다.
// ❌ 잘못된 예
auto ptr = std::make_unique<int>(42);
auto lambda = [=]() { std::cout << *ptr << "\n"; }; // 컴파일 에러
// ✅ 올바른 예
auto lambda = [p = std::move(ptr)]() { std::cout << *p << "\n"; };
에러 2: 초기화 캡처 후 원본 사용
증상: 이동 후 ptr을 사용하면 정의되지 않은 동작(UB) 또는 null 참조.
원인: std::move(ptr) 후 ptr은 빈 상태입니다.
// ❌ 잘못된 예
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() { /* ... */ };
std::cout << *ptr << "\n"; // UB: ptr은 이미 빈 상태
// ✅ 올바른 예
auto lambda = [p = std::move(ptr)]() { std::cout << *p << "\n"; };
lambda();
에러 3: IIFE 반환 타입 생략 시 추론 실패
증상: 복잡한 분기에서 반환 타입이 달라지면 컴파일 에러.
원인: 컴파일러가 여러 return 경로의 공통 타입을 추론하지 못할 수 있습니다.
// ❌ 타입이 다를 수 있음
const auto x = [&]() {
if (flag) return 1;
return std::string("hello"); // int vs string
}();
// ✅ 명시적 반환 타입
const auto x = [&]() -> std::variant<int, std::string> {
if (flag) return 1;
return std::string("hello");
}();
에러 4: 재귀 람다에서 참조 캡처 수명
증상: std::function을 지역 변수로 두고 람다를 반환하면, 반환된 람다가 나중에 호출될 때 std::function이 이미 소멸해 댕글링 참조.
// ❌ 위험
std::function<int(int)> makeFactorial() {
std::function<int(int)> factorial;
factorial = [&factorial](int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
};
return factorial; // factorial 참조가 댕글링됨!
}
// ✅ shared_ptr로 수명 연장 (또는 Y combinator)
std::function<int(int)> makeFactorial() {
auto factorial = std::make_shared<std::function<int(int)>>();
*factorial = [factorial](int n) {
if (n <= 1) return 1;
return n * (*factorial)(n - 1);
};
return *factorial;
}
에러 5: 완벽 전달 시 decltype 누락
증상: std::forward에 잘못된 타입을 넘기면 lvalue/rvalue가 보존되지 않음.
// ❌ 잘못된 예
auto bad = {
return std::forward<decltype(x)>(x); // x는 이미 lvalue
};
// ✅ 올바른 예: 인자를 forwarding reference로
auto good = -> decltype(auto) {
return std::forward<decltype(x)>(x);
};
에러 6: 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 { /* ... */ });
에러 7: std::set/priority_queue에 람다 전달 시 생성자 누락
증상: error: use of deleted default constructor 또는 error: no matching constructor.
원인: std::set<T, Compare>와 std::priority_queue<T, Container, Compare>는 비교자 타입의 기본 생성자가 없으면, 생성자에 비교자 인스턴스를 넘겨야 합니다.
// ❌ 잘못된 예
auto cmp = { return a > b; };
std::set<int, decltype(cmp)> s; // 에러: cmp는 기본 생성 불가
// ✅ 올바른 예
std::set<int, decltype(cmp)> s(cmp);
std::priority_queue<int, std::vector<int>, decltype(cmp)> pq(cmp);
에러 8: IIFE에서 void 반환
증상: error: void value not ignored as it ought to be 또는 예상치 못한 동작.
원인: IIFE의 () 뒤에 ;를 붙이면 문장이 되고, 반환값을 사용하지 않습니다. void를 반환하는 람다를 IIFE로 쓰면 ()만 호출해도 됩니다.
// ✅ void 반환 IIFE: 초기화 부수 효과만 수행
[&]() {
initializeSomething();
return; // void
}();
// ❌ 잘못된 사용
auto x = [&]() { return; }(); // x는 void
에러 요약표
| 에러 | 원인 | 해결법 |
|---|---|---|
| unique_ptr 복사 | [=] 캡처 | [p=std::move(ptr)] |
| 이동 후 원본 사용 | ptr이 빈 상태 | 이동 후 원본 사용 금지 |
| IIFE 반환 타입 | 분기별 타입 상이 | -> ReturnType 명시 |
| 재귀 람다 댕글링 | [&]로 std::function 캡처 후 반환 | shared_ptr 또는 Y combinator |
| forward 타입 오류 | auto x (값) | auto&& x (forwarding ref) |
| mutable + 멀티스레드 | 공유 람다 객체 수정 | 스레드마다 람다 복사 |
| set/pq 생성자 | 람다 기본 생성 불가 | 생성자에 람다 인스턴스 전달 |
| IIFE void 반환 | void를 변수에 대입 | 부수 효과만 수행 시 그대로 사용 |
7. 베스트 프랙티스
1. unique_ptr/큰 객체는 초기화 캡처로 이동
복사 비용이 크거나 복사 불가인 타입은 [p = std::move(ptr)]로 넘깁니다.
auto task = [p = std::move(uniquePtr)]() { use(*p); };
2. 제네릭 람다 전달 시 완벽 전달
래퍼나 데코레이터에서 인자를 그대로 넘길 때 auto&&와 std::forward를 사용합니다.
return [f = std::forward<F>(f)](auto&&... args) -> decltype(auto) {
return f(std::forward<decltype(args)>(args)...);
};
3. 복잡한 const 초기화는 IIFE
조건 분기나 여러 단계가 있는 초기화는 IIFE로 한 블록에서 처리합니다.
const auto config = [&]() {
// 복잡한 로직
return result;
}();
4. 재귀는 필요할 때만 std::function
성능이 중요하면 Y combinator나 일반 재귀 함수를 고려하고, std::function은 편의성 우선일 때 사용합니다.
5. 초기화 캡처와 기존 캡처 혼합 시 순서
[=, p = std::move(ptr)]에서 =가 먼저 오면 나머지 변수는 값 캡처, p만 초기화 캡처입니다.
6. 콜백 저장 시 템플릿 vs std::function
한 번만 호출하는 콜백(예: std::sort의 비교자)은 템플릿으로 받아 인라인 최적화를 활용합니다. 여러 타입의 콜백을 저장해야 할 때만 std::function을 사용합니다.
// ✅ 성능 중요: 템플릿
template <typename Compare>
void sortWith(Compare cmp) { std::sort(...); }
// ✅ 저장 필요: std::function
std::vector<std::function<void()>> callbacks;
7. 상태 보유 람다의 스레드 안전성
mutable 람다를 여러 스레드에서 호출할 때, 같은 람다 객체를 공유하면 데이터 레이스가 발생합니다. 스레드마다 복사하거나, 내부에서 std::mutex로 보호합니다.
8. 람다 본문은 짧게 유지
5줄을 넘어가면 별도 함수나 auto compareItems = ...로 분리해 가독성을 높입니다. 복잡한 로직은 람다보다 명명된 함수가 유지보수에 유리합니다.
8. 프로덕션 패턴
패턴 A: 작업 큐에 소유권 이전
void enqueue(std::queue<std::function<void()>>& q, std::unique_ptr<Job> job) {
q.push([p = std::move(job)]() {
p->execute();
});
}
패턴 B: 비동기 결과를 move로 전달
auto future = std::async(std::launch::async, [data = prepareLargeData()]() {
return process(std::move(data));
});
패턴 C: IIFE로 설정 검증
const auto validatedConfig = [&]() -> Config {
auto c = loadConfig();
if (!c.validate()) throw std::runtime_error("Invalid config");
c.applyDefaults();
return c;
}();
패턴 D: 완벽 전달 미들웨어
template <typename Handler>
auto withLogging(Handler&& h) {
return [h = std::forward<Handler>(h)](auto&&... args) -> decltype(auto) {
log("enter");
auto result = h(std::forward<decltype(args)>(args)...);
log("exit");
return result;
};
}
패턴 E: 재귀 visit 패턴
template <typename T>
void visitTree(T* node, std::function<void(T*)> visitor) {
std::function<void(T*)> visit;
visit = [&visit, &visitor](T* n) {
if (!n) return;
visitor(n);
for (auto* child : n->children) {
visit(child);
}
};
visit(node);
}
패턴 F: 상태 보유 람다로 시퀀스 생성
template <typename T>
auto makeSequenceGenerator(T start = T{}) {
return [n = start]() mutable { return n++; };
}
// 사용
auto gen = makeSequenceGenerator(100);
int a = gen(); // 100
int b = gen(); // 101
패턴 G: 템플릿 콜백으로 인라인 유지
template <typename Container, typename Predicate>
auto findIf(Container& c, Predicate pred) {
return std::find_if(std::begin(c), std::end(c), pred);
}
// 람다가 인라인됨
auto it = findIf(vec, [threshold](int x) { return x > threshold; });
패턴 H: std::function 콜백 저장 (수명 관리)
class AsyncWorker {
std::function<void()> onComplete_;
public:
void setOnComplete(std::function<void()> fn) {
onComplete_ = std::move(fn);
}
void finish() {
if (onComplete_) onComplete_();
}
};
// 값 캡처로 수명 안전
int result = 42;
worker.setOnComplete([result]() { saveResult(result); });
패턴 I: 람다 비교자로 정렬 기준 주입
enum class SortBy { Name, Age, Score };
void sortPeople(std::vector<Person>& people, SortBy by) {
switch (by) {
case SortBy::Name:
std::sort(people.begin(), people.end(),
{ return a.name < b.name; });
break;
case SortBy::Age:
std::sort(people.begin(), people.end(),
{ return a.age < b.age; });
break;
case SortBy::Score:
std::sort(people.begin(), people.end(),
{ return a.score > b.score; });
break;
}
}
9. 구현 체크리스트
람다 고급 기능을 쓸 때 확인할 항목입니다.
- unique_ptr/이동 전용:
[p = std::move(ptr)]사용 - 이동 후 원본: 사용하지 않기
- 완벽 전달:
auto&&+std::forward<decltype(x)>(x) - IIFE 반환 타입: 분기 시
-> ReturnType명시 - 재귀 람다: 반환 시
[&]캡처 주의, shared_ptr 또는 Y combinator - 큰 객체: 복사 대신 move 캡처
- 상태 보유 람다: mutable + 초기화 캡처, 멀티스레드 시 복사
- 템플릿 vs std::function: 저장 필요 시에만 std::function
- set/priority_queue: 생성자에 람다 인스턴스 전달
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
- C++ Perfect Forwarding | std::forward로 “복사 없이 인자 전달”
이 글에서 다루는 키워드 (관련 검색어)
C++ 람다 심화, 초기화 캡처, init capture, unique_ptr 람다, 완벽 전달, IIFE, 재귀 람다, std::forward 람다, 상태 보유 람다, 람다 비교자, 템플릿 람다, std::function 람다 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 문법 | 용도 |
|---|---|---|
| 초기화 캡처 | [p = std::move(ptr)] | unique_ptr, 큰 객체 이동 |
| 완벽 전달 | auto&& + std::forward | 인자 그대로 전달 |
| IIFE | { }() | const 조건부 초기화, 스코프 제한 |
| 재귀 람다 | std::function + [&] | 자기 호출 |
| 상태 보유 람다 | [n = 0]() mutable | 카운터, 시퀀스 생성기 |
| 람다 비교자 | “ | sort, set, priority_queue |
| 템플릿 람다 | template<typename F> void f(F&&) | 인라인 최적화 |
핵심 원칙:
- 이동 전용 타입은 초기화 캡처
- 래퍼에서는 완벽 전달
- 복잡한 초기화는 IIFE
- 재귀는 std::function 또는 Y combinator
- 콜백 저장 시에만 std::function, 그 외에는 템플릿
참고 자료
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. unique_ptr을 비동기 큐에 넘길 때, 제네릭 래퍼에서 인자를 그대로 전달할 때, const 변수를 조건에 따라 초기화할 때, 람다로 재귀를 표현할 때 위 패턴들을 사용합니다.
Q. 선행으로 읽으면 좋은 글은?
A. 람다 기초(#10-1)에서 캡처·mutable·제네릭 람다를 먼저 익히면 좋습니다. 완벽 전달(#14-2)도 함께 보면 이해가 빠릅니다.
Q. 더 깊이 공부하려면?
A. cppreference의 람다 표현식, std::forward 문서를 참고하세요. Y combinator는 함수형 프로그래밍 자료에서 자세히 다룹니다.
Q. std::function 재귀와 Y combinator 중 뭘 써야 하나요?
A. 구현이 간단한 std::function은 프로토타입이나 성능이 중요하지 않은 경로에, Y combinator는 인라인과 성능이 중요한 경로에 사용하는 것을 권합니다.
한 줄 요약: 초기화 캡처로 unique_ptr을 넘기고, 완벽 전달로 인자를 보존하며, IIFE로 초기화를 단순화하고, 재귀 람다로 트리·그래프를 순회할 수 있습니다. 다음으로 STL 알고리즘(#10-3)과 람다 표현식 심화(#13-1)를 읽어보면 좋습니다.
다음 글: C++ 실전 가이드 #10-3: STL 알고리즘
이전 글: C++ 실전 가이드 #10-1: 람다 기초
관련 글
- C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
- C++ vector 성능 |
- C++ map vs unordered_map (STL 시리즈) |
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
- C++ 템플릿 입문 | template
와 템플릿 컴파일 에러 해결법