C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴

C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴

이 글의 핵심

C++ 람다 심화에 대한 실전 가이드입니다. 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴 등을 예제와 함께 상세히 설명합니다.

들어가며: unique_ptr을 스레드에 넘기려면 어떻게 해야 할까요?

”소유권을 넘기고 싶은데 [=]로는 복사가 안 되고, [&]로는 댕글링이…”

비동기 작업 큐에 unique_ptr을 담은 작업을 넣고 싶었습니다. std::unique_ptr복사가 불가능하고 이동만 가능합니다. [=]로 캡처하면 복사 시도로 컴파일 에러가 나고, [&]로 캡처하면 지역 변수가 스코프를 벗어난 뒤 큐에서 작업이 실행될 때 댕글링 참조가 됩니다.

자주 겪는 문제 시나리오들

시나리오 1: unique_ptr을 람다로 넘기기
std::threadstd::asyncunique_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

목차

  1. 초기화 캡처 (Init Capture)
  2. 완벽 전달 (Perfect Forwarding)
  3. IIFE (즉시 실행 함수 표현식)
  4. 재귀 람다 (Recursive Lambda)
  5. 완전한 람다 고급 예제 모음 (상태 보유·템플릿·비교자·std::function)
  6. 자주 발생하는 에러와 해결법
  7. 베스트 프랙티스
  8. 프로덕션 패턴
  9. 구현 체크리스트

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&&)인라인 최적화

핵심 원칙:

  1. 이동 전용 타입은 초기화 캡처
  2. 래퍼에서는 완벽 전달
  3. 복잡한 초기화는 IIFE
  4. 재귀는 std::function 또는 Y combinator
  5. 콜백 저장 시에만 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와 템플릿 컴파일 에러 해결법