본문으로 건너뛰기
Previous
Next
C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴

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

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

이 글의 핵심

C++ 람다 기초 : 캡처·mutable·제네릭 람다와 실전 패턴. 정렬 기준을 바꾸려면 클래스를 만들어야 하나요?·실무에서 겪은 문제.

💡 초보자를 위한 순서: [](int a, int b){ return a < b; }처럼 인자를 적는 람다부터 익힌 뒤, [=] / [&]는 “바깥 변수를 어떻게 가져올지”만 추가하면 됩니다. 스레드·비동기로 넘길 땐 [&]에 지역 변수를 잡지 말고 값 캡처를 우선 의심하세요. 상세는 본문 구현 체크리스트를 보세요.

들어가며: 정렬 기준을 바꾸려면 클래스를 만들어야 하나요?

”한 줄 비교 로직인데 왜 이렇게 복잡해요?”

벡터를 나이순으로 정렬하려고 했습니다. 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

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

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. 나중 호출 시 값 캡처

초보자를 위한 체크리스트

  • std::sort 등에 넘길 비교 람다가 bool 반환인지 (< 연산자 규약)
  • [&]로 잡은 변수가 람다보다 먼저 파괉되지 않는지 (스레드·콜백 지연 호출)
  • mutable값 캡처를 람다 안에서 바꿀 때만 필요한지 확인했는가?

💡 초보자 팁: 막히면 10절 구현 체크리스트부터 위에서 아래로 체크해 보세요.


참고 자료

자주 묻는 질문 (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 람다와 함께 쓰기 (실전 패턴)

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.