C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴

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가 끝난 뒤 콜백에서 “요청 시점의” requestIduserId를 사용해야 합니다. 참조 캡처 [&]를 쓰면 스택이 해제된 뒤 댕글링이 되고, 값 캡처 [=]로 필요한 변수만 복사해야 합니다.

시나리오 6: 조건에 따라 다른 초기화
const 변수를 if 분기마다 다른 값으로 초기화하고 싶을 때, IIFE를 쓰면 분기 로직을 한 블록에 담아 const config = [&](){ ... }(); 형태로 깔끔하게 처리할 수 있습니다.

정의를 풀어 쓰면 람다(lambda)는 “이름 없는 작은 함수”를 호출하는 자리에서 바로 정의하는 문법입니다. 비유하면 필요한 순간에만 쓰는 임시 메모 같은 것입니다. sort·find_if·스레드 생성처럼 콜백(나중에 호출될 함수를 인자로 넘기는 방식)이 필요한 STL·API에서 자주 쓰입니다.

문제의 코드:

struct CompareByAge {
    bool operator()(const Person& a, const Person& b) const {
        return a.age < b.age;
    }
};

std::vector<Person> people = { /* ... */ };
std::sort(people.begin(), people.end(), CompareByAge());

람다로 해결:

std::vector<Person> people = { /* ... */ };

std::sort(people.begin(), people.end(),
     {
        return a.age < b.age;
    });

이 글을 읽으면:

  • 람다 표현식의 기본 문법을 이해할 수 있습니다.
  • 캡처 방식(값, 참조)을 올바르게 사용할 수 있습니다.
  • mutable, 제네릭 람다를 활용할 수 있습니다.
  • 자주 겪는 에러와 해결법을 알 수 있습니다.
  • 프로덕션에서의 람다 패턴을 배울 수 있습니다.

람다의 캡처와 실행 흐름을 요약하면 아래와 같습니다.

flowchart TB
  subgraph capture["캡처 시점"]
    A[람다 정의] --> B{캡처 모드}
    B --> C[""(="] 값 복사"]
    B --> D[""(&"] 참조"]
    B --> E[""(x, &y"] 혼합"]
  end
  subgraph exec["실행 시점"]
    F[람다 호출] --> G{캡처 타입}
    G --> H["값: 스냅샷 사용"]
    G --> I["참조: 현재 값 접근"]
  end
  capture --> exec

목차

  1. 람다 기초 문법
  2. 캡처 방식 완전 정리
  3. mutable과 예외 지정
  4. 제네릭 람다 (C++14)
  5. 재귀 람다
  6. 완전한 람다 예제 모음
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트

1. 람다 기초 문법

기본 형태

람다는 캡처 [ ], 매개변수 ( ), 반환 타입 -> return_type(생략 가능), 본문 { } 네 부분으로 이루어집니다.

[capture](parameters) -> return_type {
    // 함수 본문
}

최소 예제

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o lambda_basic lambda_basic.cpp && ./lambda_basic
#include <iostream>

int main() {
    auto add =  -> int {
        return a + b;
    };
    int result = add(3, 5);  // 8
    std::cout << result << "\n";
    return 0;
}

실행 결과:

8

위 코드 설명: []는 캡처가 없음을 의미합니다. (int a, int b)는 일반 함수와 동일한 매개변수입니다. -> int는 반환 타입을 명시하며, 생략하면 컴파일러가 return a + b로부터 추론합니다.

반환 타입 생략

-> return_type을 쓰지 않으면 컴파일러가 return문의 식으로부터 반환 타입을 추론합니다.

auto add =  {
    return a + b;  // int 반환
};

auto multiply =  {
    return a * b;  // double 반환
};

매개변수 없는 람다

매개변수가 없으면 ()만 쓰거나, C++11 이후에서는 괄호를 아예 생략할 수 있습니다.

auto sayHello =  {
    std::cout << "Hello!\n";
};

auto sayWorld = [] {
    std::cout << "World!\n";
};

즉시 실행 람다 (IIFE)

람다를 정의한 뒤 바로 (인자)를 붙여 호출하면, “한 번만 쓰는 함수”를 인라인으로 실행할 수 있습니다. IIFE(Immediately Invoked Function Expression)는 JavaScript에서 유래한 용어로, C++에서도 복잡한 초기화·스코프 분리·const 변수 초기화에 유용합니다.

// 기본 IIFE: 즉시 계산
int result =  {
    return x * x;
}(5);  // 25

// 복잡한 초기화를 const 변수에
auto data =  {
    std::vector<int> vec;
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i * i);
    }
    return vec;
}();

// 스코프 분리: 임시 변수 노출 방지
int value = [&]() {
    int temp1 = computeA();
    int temp2 = computeB();
    return temp1 + temp2;  // temp1, temp2는 외부에 노출되지 않음
}();

IIFE 활용: const 분기 초기화, 스코프 분리, RAII 스코프.


2. 캡처 방식 완전 정리

값 캡처 [=]

[=]람다가 정의된 시점의 주변 변수들을 값으로 복사해 둡니다. 람다가 나중에 실행될 때(예: 콜백) 참조가 끊기지 않도록 할 때 사용합니다.

int x = 10;
int y = 20;

auto lambda = [=]() {
    std::cout << x << ", " << y << "\n";  // 10, 20
};

x = 100;  // 람다 내부의 x는 변하지 않음
lambda();  // 10, 20

참조 캡처 [&]

[&]는 주변 변수를 참조로 캡처합니다. 람다 안에서 수정하면 원본이 바뀝니다. 즉시 호출할 때만 안전합니다.

int x = 10;
int y = 20;

auto lambda = [&]() {
    x += 5;
    y += 10;
};

lambda();
std::cout << x << ", " << y << "\n";  // 15, 30

선택적 캡처

캡처할 변수를 이름으로 나열하면, [x]는 값, [&y]는 참조로만 캡처합니다.

int x = 10;
int y = 20;
int z = 30;

auto lambda = [x, &y]() {
    std::cout << x << ", " << y << "\n";
    // std::cout << z;  // ❌ 에러: z 캡처 안 됨
};

혼합 캡처

[=, &y]기본은 값 캡처이고 y만 참조로, [&, x]기본은 참조이고 x만 값으로 캡처합니다.

int x = 10;
int y = 20;
int z = 30;

auto lambda1 = [=, &y]() {
    std::cout << x << ", " << y << ", " << z << "\n";
};

auto lambda2 = [&, x]() {
    std::cout << x << ", " << y << ", " << z << "\n";
};

this 캡처

멤버 함수 안에서 람다를 쓰고 멤버 변수에 접근하려면 [this]로 현재 객체를 캡처합니다.

class Counter {
    int count = 0;

public:
    void increment() {
        auto lambda = [this]() {
            count++;
        };
        lambda();
    }

    int getCount() const { return count; }
};

초기화 캡처 (C++14)

[이름 = 식]람다 전용 변수를 만들어 식의 결과로 초기화합니다. [p = std::move(ptr)]처럼 move로 가져올 수 있습니다.

int x = 10;

auto lambda = [y = x + 5]() {
    std::cout << y << "\n";  // 15
};

auto ptr = std::make_unique<int>(42);
auto lambda2 = [p = std::move(ptr)]() {
    std::cout << *p << "\n";
};

캡처 모드 비교표

캡처 문법의미수명 안전사용 시점
[]아무것도 캡처 안 함항상 안전외부 변수 불필요
[=]모든 변수 값 복사나중 호출 시 안전비동기·스레드·저장용 콜백
[&]모든 변수 참조즉시 호출만 안전동기 콜백, sort·find_if
[x]x만 값 복사나중 호출 시 안전필요한 변수만 선택
[&y]y만 참조즉시 호출만 안전수정 필요할 때
[=, &y]기본 값, y만 참조y 수명 주의대부분 복사, y만 갱신
[&, x]기본 참조, x만 값x는 안전대부분 참조, x만 스냅샷
[this]현재 객체 포인터객체 수명 주의멤버 함수 내부
[*this] (C++17)객체 전체 복사나중 호출 시 안전스레드로 객체 넘길 때
[p = std::move(ptr)]move로 초기화소유권 이전unique_ptr, 큰 객체

3. mutable과 예외 지정

mutable 람다

값으로 캡처한 변수는 람다 안에서 const로 취급되어 수정할 수 없습니다. mutable을 붙이면 그 복사본은 수정 가능해지지만, 원본은 변하지 않습니다.

int x = 0;

auto lambda1 = [x]() {
    // x++;  // ❌ 에러: const
    std::cout << x << "\n";
};

auto lambda2 = [x]() mutable {
    x++;  // ✅ OK (복사본 수정)
    std::cout << x << "\n";
};

lambda2();  // 1
lambda2();  // 2
std::cout << x << "\n";  // 0 (원본은 변하지 않음)

위 코드 설명: mutable은 람다의 operator()를 비const로 만듭니다. 값 캡처된 변수는 람다 객체의 멤버로 저장되며, 호출할 때마다 그 복사본이 갱신됩니다. 원본 x는 영향을 받지 않습니다.

noexcept 지정

noexcept를 붙이면 이 람다가 예외를 던지지 않는다고 선언합니다.

auto lambda =  noexcept {
    return 42;
};

속성 지정

auto lambda =  [[nodiscard]] {
    return 42;
};

4. 제네릭 람다 (C++14)

auto 매개변수

C++14부터 람다 매개변수에 auto를 쓸 수 있어서, 호출될 때마다 인자 타입에 맞는 템플릿 인스턴스가 생성됩니다.

auto print =  {
    std::cout << value << "\n";
};

print(42);       // int
print(3.14);     // double
print("hello");  // const char*

여러 타입 매개변수

auto add =  {
    return a + b;
};

std::cout << add(1, 2) << "\n";        // 3
std::cout << add(1.5, 2.5) << "\n";    // 4.0
std::cout << add(std::string("Hello"), std::string(" World")) << "\n";

템플릿 람다 (C++20)

C++20에서는 람다에 템플릿 매개변수를 직접 쓸 수 있습니다.

auto lambda = []<typename T>(T value) {
    std::cout << typeid(T).name() << ": " << value << "\n";
};

lambda(42);      // int: 42
lambda(3.14);    // double: 3.14

4.5. 재귀 람다 (Recursive Lambda)

람다는 이름이 없어서 자기 자신을 직접 호출할 수 없습니다. 재귀를 구현하려면 std::function에 담거나, 초기화 캡처로 자기 자신을 캡처해야 합니다.

std::function을 이용한 재귀

#include <functional>
#include <iostream>

int main() {
    std::function<int(int)> factorial;
    factorial = [&factorial](int n) -> int {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    };

    std::cout << factorial(5) << "\n";  // 120
    return 0;
}

주의: factorial참조로 캡처해야 합니다. 값 캡처 [=]를 쓰면 복사 시점에 아직 초기화되지 않은 factorial이 복사되어 댕글링이 됩니다.

Y 컴비네이터 스타일 (고급)

std::function 없이 재귀를 구현하려면 고차 함수를 이용합니다. 실무에서는 std::function 방식이 더 읽기 쉽습니다.

// Y 컴비네이터: 람다가 자기 자신을 인자로 받음
auto factorial =  -> int {
    if (n <= 1) return 1;
    return n * self(self, n - 1);
};

std::cout << factorial(factorial, 5) << "\n";  // 120

재귀 람다 사용 시나리오

  • 트리/그래프 순회: 노드 방문 시 자식에게 같은 람다 전달
  • JSON/XML 파싱: 중첩 구조 재귀 처리
  • 수학적 정의: 팩토리얼, 피보나치 등

6. 완전한 람다 예제 모음

예제 1: STL 알고리즘 (find_if, count_if, transform)

// g++ -std=c++17 -o lambda_stl lambda_stl.cpp && ./lambda_stl
#include <algorithm>
#include <iostream>
#include <vector>

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"Alice", 25}, {"Bob", 30}, {"Charlie", 20}, {"Diana", 25}
    };
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto it = std::find_if(people.begin(), people.end(),
         { return p.age >= 25; });
    if (it != people.end())
        std::cout << "Found: " << it->name << ", " << it->age << "\n";

    int count = std::count_if(people.begin(), people.end(),
         { return p.age >= 25; });
    std::cout << "Count (age>=25): " << count << "\n";

    bool allAdult = std::all_of(people.begin(), people.end(),
         { return p.age >= 18; });
    std::cout << "All adult: " << (allAdult ? "yes" : "no") << "\n";

    std::vector<int> squares;
    std::transform(numbers.begin(), numbers.end(),
        std::back_inserter(squares),
         { return x * x; });
    std::cout << "Squares: ";
    for (int s : squares) std::cout << s << " ";
    std::cout << "\n";

    return 0;
}

실행 결과:

Found: Alice, 25
Count (age>=25): 3
All adult: yes
Squares: 1 4 9 16 25 36 49 64 81 100

예제 2: sort와 캡처 활용

struct Person {
    std::string name;
    int age;
};

std::vector<Person> people = {
    {"Alice", 25}, {"Bob", 30}, {"Charlie", 20}
};

int minAge = 18;

std::sort(people.begin(), people.end(),
     {
        return a.age < b.age;
    });

std::sort(people.begin(), people.end(),
    [minAge](const Person& a, const Person& b) {
        return a.age < b.age && a.age >= minAge;
    });

예제 3: std::thread와 값 캡처

// g++ -std=c++17 -pthread -o lambda_thread lambda_thread.cpp && ./lambda_thread
#include <iostream>
#include <thread>
#include <vector>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    int multiplier = 10;

    std::thread t([data, multiplier]() {
        for (int x : data) {
            std::cout << x * multiplier << " ";
        }
        std::cout << "\n";
    });

    t.join();
    return 0;
}

실행 결과:

10 20 30 40 50

예제 4: mutable 카운터

int main() {
    auto counter = [n = 0]() mutable {
        return ++n;
    };

    std::cout << counter() << "\n";  // 1
    std::cout << counter() << "\n";  // 2
    std::cout << counter() << "\n";  // 3
}

예제 5: 제네릭 람다로 타입별 처리

auto process =  {
    if constexpr (std::is_integral_v<decltype(value)>) {
        return value * 2;
    } else if constexpr (std::is_floating_point_v<decltype(value)>) {
        return value * 1.5;
    } else {
        return value;
    }
};

std::cout << process(10) << "\n";    // 20
std::cout << process(10.0) << "\n";  // 15

예제 6: 초기화 캡처로 unique_ptr 이동

#include <memory>
#include <iostream>

int main() {
    auto ptr = std::make_unique<int>(42);

    // [p = std::move(ptr)]: 소유권 이전
    auto lambda = [p = std::move(ptr)]() {
        std::cout << *p << "\n";
    };

    lambda();  // 42
    // ptr은 이제 nullptr (이동됨)
    return 0;
}

예제 8: IIFE로 const 분기 초기화

#include <iostream>
#include <string>

int main() {
    bool useCache = true;

    // const 변수를 조건에 따라 다르게 초기화
    const std::string config = [&]() {
        if (useCache) {
            return std::string("cache_enabled");
        } else {
            return std::string("cache_disabled");
        }
    }();

    std::cout << config << "\n";  // cache_enabled
    return 0;
}

예제 8: [*this]로 객체 복사 (C++17)

[this]는 객체 소멸 시 댕글링 위험이 있습니다. [*this]로 객체 전체를 복사하면 스레드가 독립된 복사본을 갖습니다.

#include <thread>
#include <iostream>

struct Worker {
    int id = 0;

    void runAsync() {
        // [*this]: 객체 복사, 스레드가 종료될 때까지 안전
        std::thread t([*this]() {
            std::cout << "Worker " << id << " running\n";
        });
        t.detach();
    }
};

int main() {
    Worker w;
    w.id = 42;
    w.runAsync();
    // main 종료 후에도 스레드는 복사본으로 동작
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return 0;
}

7. 자주 발생하는 에러와 해결법

에러 1: 댕글링 참조 (Dangling Reference)

증상: 프로그램이 크래시하거나, 이상한 값이 출력되거나, 릴리즈 빌드에서만 문제가 발생합니다.

원인: 참조로 캡처한 지역 변수가 스코프를 벗어나 소멸한 뒤, 람다가 실행됩니다.

// ❌ 잘못된 예
std::function<void()> createBroken() {
    int x = 42;
    return [&x]() { std::cout << x << "\n"; };  // x는 createBroken() 종료 시 소멸
}

// ✅ 올바른 예
std::function<void()> createSafe() {
    int x = 42;
    return [x]() { std::cout << x << "\n"; };
}

에러 2: for 루프 변수 캡처

증상: 스레드나 콜백에서 루프 인덱스 i가 항상 마지막 값만 나옵니다.

원인: [&i]로 참조 캡처하면 모든 람다가 같은 i를 가리키고, 루프가 끝난 뒤 i는 최종값입니다.

// ❌ 잘못된 예
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
    threads.emplace_back([&i]() {
        std::cout << i << "\n";  // 모두 5 또는 undefined
    });
}

// ✅ 올바른 예
for (int i = 0; i < 5; ++i) {
    threads.emplace_back([i]() {
        std::cout << i << "\n";  // 0, 1, 2, 3, 4
    });
}

에러 3: mutable 없이 값 캡처 수정 시도

증상: error: increment of read-only variable 'x' 컴파일 에러.

원인: 값 캡처된 변수는 람다 내부에서 const로 취급됩니다.

// ❌ 잘못된 예
int x = 0;
auto lambda = [x]() {
    x++;  // 컴파일 에러
};

// ✅ 올바른 예
auto lambda = [x]() mutable {
    x++;  // 복사본만 수정
};

에러 4: this 캡처 후 객체 소멸

증상: 멤버 함수에서 람다를 스레드나 큐에 넘긴 뒤, 객체가 먼저 소멸하면 댕글링 포인터 접근으로 크래시합니다.

// ❌ 위험
class Worker {
    void start() {
        std::thread t([this]() {
            this->doWork();  // Worker 소멸 후 호출 시 UB
        });
        t.detach();
    }
};

// ✅ 안전: shared_from_this 또는 [*this] (C++17)
class Worker : public std::enable_shared_from_this<Worker> {
    void start() {
        auto self = shared_from_this();
        std::thread t([self]() { self->doWork(); });
        t.detach();
    }
};

에러 5: [=]로 큰 객체 불필요 복사

증상: 메모리 사용량 증가, 성능 저하.

원인: [=]는 사용하지 않는 변수까지 모두 복사할 수 있습니다.

std::string big(10000, 'x');
int threshold = 10;

// ❌ big 전체 복사
auto bad = [=]() { return big.size() > threshold; };

// ✅ 참조 + 값
auto good = [&big, threshold]() { return big.size() > threshold; };

에러 6: 초기화 캡처 순서 오류

증상: error: 'x' was not declared in this scope 또는 예상치 못한 값.

원인: 초기화 캡처 [a = b]에서 b캡처 리스트보다 앞에 선언되어 있어야 합니다. [a = b, b = a]처럼 순환 참조는 불가능합니다.

int x = 10;

// ❌ y가 아직 정의되지 않음
// auto bad = [z = y, y = x]() { return z + y; };

// ✅ 올바른 순서
auto good = [y = x]() { return y * 2; };

에러 7: std::function 시그니처 불일치

증상: 컴파일 에러 또는 런타임 크래시.

원인: std::function의 반환/인자 타입과 람다가 맞지 않을 때.

// ❌ std::function<int()> fn =  { std::cout << "hi"; };  // void 반환
// ✅ std::function<void()> fn =  { std::cout << "hi\n"; };

에러 8: mutable 람다를 여러 스레드에서 동시 호출

증상: 데이터 레이스, UB.

원인: mutable 람다의 캡처 변수는 람다 객체 멤버입니다. 여러 스레드가 같은 람다를 공유하면 동시 수정이 발생합니다.

// ❌ 참조로 공유 → 데이터 레이스
auto counter = [n = 0]() mutable { return ++n; };
std::thread t1([&counter]() { for (int i = 0; i < 100; ++i) counter(); });
std::thread t2([&counter]() { for (int i = 0; i < 100; ++i) counter(); });

// ✅ 스레드마다 람다 복사
std::thread t1([counter]() mutable { /* ... */ });
std::thread t2([counter]() mutable { /* ... */ });

에러 요약표

에러원인해결법
댕글링 참조참조 캡처 후 스코프 종료값 캡처 [x]
for 루프 i[&i] 참조 캡처[i] 값 캡처
값 캡처 수정const 취급mutable 추가
this 소멸객체가 먼저 소멸shared_from_this, [*this]
불필요 복사[=] 과다 사용선택 캡처 [&big, x]
초기화 캡처 순서미정의 변수 참조선언 순서 확인
std::function 타입시그니처 불일치반환/인자 타입 일치
mutable + 멀티스레드공유 람다 객체 수정스레드마다 복사

성능 비교: 람다 vs std::function vs 함수 포인터

방식인라인 가능힙 할당오버헤드사용 시점
람다 (템플릿 전달)없음콜백을 한 번만 받을 때
람다 (std::function 저장)✅ 가능호출당 간접 호출타입 소거 필요 시
함수 포인터간접 호출C API 연동
함수 객체없음상태가 필요할 때

실무 팁: std::sort(v.begin(), v.end(), { return a < b; })처럼 람다를 템플릿 인자로 직접 넘기면 컴파일러가 인라인 최적화를 수행합니다. std::function<bool(int,int)>로 받으면 타입이 소거되어 인라인이 어렵고, 작은 람다도 힙에 저장될 수 있습니다.


8. 베스트 프랙티스

1. 캡처 선택 가이드

  • 즉시 호출(sort, find_if): [&] 또는 [=] 모두 안전
  • 나중 호출(스레드, async, 콜백 저장): [=] 또는 [x, y] 값 캡처
  • 큰 객체: [&] 또는 [s = std::move(str)] move 캡처

2. std::function 대신 템플릿

std::function은 힙 할당을 할 수 있어 오버헤드가 있습니다. 콜백을 한 번만 받는다면 template<typename F>로 람다를 직접 받는 편이 좋습니다.

// ❌ 매 호출마다 function 오버헤드
void sortWithFunc(std::vector<int>& v, std::function<bool(int,int)> cmp);

// ✅ 인라인 가능, 오버헤드 없음
template <typename Compare>
void sortWithLambda(std::vector<int>& v, Compare cmp) {
    std::sort(v.begin(), v.end(), cmp);
}

3. 필요한 것만 캡처

[=]는 사용하지 않는 변수까지 복사할 수 있으므로, 필요한 변수만 [a, &b]로 선택 캡처합니다.

4. noexcept 람다

예외를 던지지 않는 람다에 noexcept를 붙이면, 이동 연산 등에서 컴파일러가 더 나은 코드를 생성할 수 있습니다.

auto safe =  noexcept { return 42; };

5. 즉시 실행 람다로 복잡 초기화

복잡한 초기화를 한 번만 수행하고, 결과만 변수에 넣을 때 즉시 실행 람다를 사용합니다.

auto config = [&]() {
    Config c;
    c.loadFromFile("config.json");
    c.merge(defaults);
    return c;
}();

6. 람다 본문은 짧게 유지

5줄을 넘어가면 별도 함수나 auto compareItems = ...로 분리해 가독성을 높입니다.

7. 캡처할 변수는 최소화

[=][&]는 모든 변수를 캡처하므로, 나중에 변수가 추가되면 의도치 않게 캡처될 수 있습니다. [a, &b]처럼 필요한 것만 명시합니다.

8. 재귀 람다는 std::function 또는 명명된 함수로

재귀가 복잡해지면 일반 함수가 더 읽기 쉽습니다. 람다로 할 때는 std::function에 담고 참조 캡처 [&f]를 사용합니다.


9. 프로덕션 패턴

패턴 A: 스레드에 안전하게 인자 전달

스레드에 넘기는 람다는 반드시 값 캡처로 필요한 데이터를 복사합니다.

void processInBackground(const std::string& input) {
    std::thread t([input]() {
        auto result = expensiveComputation(input);
        saveResult(result);
    });
    t.detach();
}

패턴 B: std::async와 람다

std::async에 람다를 넘길 때도 값 캡처를 사용합니다.

auto future = std::async(std::launch::async, [data = prepareData()]() {
    return process(data);
});
auto result = future.get();

패턴 C: ScopeGuard와 에러 처리

예외 발생 시에도 리소스를 안전하게 해제합니다.

template <typename Func>
class ScopeGuard {
    Func func;
    bool active = true;
public:
    ScopeGuard(Func f) : func(std::move(f)) {}
    ~ScopeGuard() { if (active) func(); }
    void dismiss() { active = false; }
};

void writeWithBackup(const std::string& path, const std::string& data) {
    std::string tmpPath = path + ".tmp";
    std::ofstream out(tmpPath);
    if (!out) throw std::runtime_error("Cannot open " + tmpPath);
    auto guard = ScopeGuard([tmpPath]() { std::filesystem::remove(tmpPath); });
    out << data;
    out.close();
    std::filesystem::rename(tmpPath, path);
    guard.dismiss();
}

패턴 D: 조건부 로직 캡슐화

반복되는 if-else 패턴을 람다로 묶어 전략 패턴처럼 동작을 주입합니다.

template <typename OnSuccess, typename OnFailure>
void tryOperation(OnSuccess&& onOk, OnFailure&& onFail) {
    if (doSomething()) {
        onOk();
    } else {
        onFail();
    }
}

tryOperation(
     { log("OK"); commit(); },
     { log("Failed"); rollback(); }
);

패턴 E: 콜백 저장 시 수명 관리

버튼 클릭처럼 나중에 호출될 동작을 저장할 때, 콜백이 객체보다 오래 살 수 있으면 값 캡처나 shared_ptr로 수명을 맞춥니다.

class Button {
    std::function<void()> onClick;

public:
    void setOnClick(std::function<void()> callback) {
        onClick = std::move(callback);
    }

    void click() {
        if (onClick) onClick();
    }
};

int main() {
    Button button;
    int clickCount = 0;

    button.setOnClick([clickCount]() mutable {
        clickCount++;
        std::cout << "Clicked " << clickCount << " times\n";
    });

    button.click();
    button.click();
}

주의: clickCount를 값 캡처하면 복사본이 들어가 main의 값은 변하지 않습니다. 상태 공유가 필요하면 std::shared_ptr나 클래스 멤버를 사용하세요.

패턴 F: 지연 계산 (Lazy Evaluation)

비용이 큰 계산을 필요할 때만 수행합니다.

auto getExpensiveResult = [&]() { return computeHeavyStuff(data); };
if (needResult) use(getExpensiveResult());

패턴 G: 에러 핸들러 주입

재시도·로깅을 람다로 주입해 동일한 구조를 재사용합니다.

template <typename Op, typename OnError>
auto tryWithRetry(Op op, OnError onError, int maxRetries = 3) {
    for (int i = 0; i < maxRetries; ++i) {
        try { return op(); }
        catch (const std::exception& e) {
            onError(i, e.what());
            if (i == maxRetries - 1) throw;
        }
    }
    throw std::runtime_error("Unreachable");
}
// 사용: tryWithRetry([&](){ return fetch(url); }, { ... });

패턴 H: 알고리즘 커스터마이징

비교·변환 함수를 람다로 주입해 동일한 알고리즘을 다양한 용도로 사용합니다.

std::sort(users.begin(), users.end(),
     { return a.lastLogin > b.lastLogin; });
std::transform(users.begin(), users.end(), std::back_inserter(names),
     { return u.name; });

10. 구현 체크리스트

람다를 도입할 때 확인할 항목입니다.

  • 캡처 모드: 즉시 호출인가, 나중 호출인가에 따라 [=] vs [&] 선택
  • 수명 안전: 참조 캡처 시 람다가 참조보다 오래 살지 않는지 확인
  • for 루프: [i] 값 캡처, [&i] 사용 금지
  • mutable: 값 캡처 수정 시 mutable 추가
  • this 캡처: 객체 수명이 람다보다 길어지는지 확인
  • 큰 객체: [&] 또는 [s = std::move(str)] 사용
  • std::function: 저장이 필요할 때만 사용, 그 외에는 템플릿으로 전달

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
  • C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
  • C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법

이 글에서 다루는 키워드 (관련 검색어)

C++ 람다, 람다 표현식, 캡처 [=] [&], mutable, 제네릭 람다, sort 람다, find_if, 댕글링 참조 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목문법용도
값 캡처[=]모든 변수 복사
참조 캡처[&]모든 변수 참조
선택 캡처[x, &y]x 복사, y 참조
this 캡처[this]멤버 접근
초기화 캡처[x = expr]새 변수 생성
mutable mutable값 캡처 수정
제네릭모든 타입

핵심 원칙:

  1. 짧은 로직은 람다
  2. 참조 캡처 주의 (수명)
  3. 큰 객체는 참조 또는 move
  4. STL 알고리즘과 함께 사용
  5. 나중 호출 시 값 캡처

참고 자료

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++11 람다 기초 완벽 가이드. [=]·[&] 캡처, mutable, 제네릭 람다, sort·find_if·스레드에서 람다 활용, 댕글링 참조·for 루프 캡처 등 자주 겪는 에러와 해결법. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

Q. 람다와 일반 함수의 성능 차이는?

A. 람다는 컴파일러가 인라인으로 최적화할 수 있어, std::function에 담지 않고 템플릿으로 직접 전달하면 일반 함수와 거의 동일한 성능입니다. std::function은 타입 소거를 위해 힙 할당을 할 수 있어 오버헤드가 있습니다.

한 줄 요약: 람다로 한 곳에서만 쓰는 함수를 인라인 정의해 STL과 콜백에 활용할 수 있습니다. 다음으로 STL 알고리즘(#10-3)람다 표현식 심화(#13-1)를 읽어보면 좋습니다.

다음 글: C++ 실전 가이드 #10-2: STL map과 set

이전 글: C++ 실전 가이드 #09-3: 가변 인자 템플릿


관련 글

  • C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴
  • C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
  • C++ vector 성능 |
  • C++ map vs unordered_map (STL 시리즈) |
  • C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)