C++ 람다 캡처 에러 | "dangling reference" 크래시와 캡처 실수 해결

C++ 람다 캡처 에러 | "dangling reference" 크래시와 캡처 실수 해결

이 글의 핵심

C++ 람다 캡처 에러에 대한 실전 가이드입니다.

들어가며: “람다를 저장했더니 크래시가 나요"

"참조 캡처한 변수가 이미 소멸되었어요”

C++11의 람다(Lambda)는 익명 함수를 간결하게 작성할 수 있게 해주지만, 캡처(Capture—람다가 외부 변수를 사용하는 방식)를 잘못 사용하면 댕글링 참조예상치 못한 동작이 발생합니다.

// ❌ 댕글링 참조
std::function<int()> createLambda() {
    int x = 42;
    return [&x]() { return x; };  // x를 참조 캡처
}  // x 소멸

int main() {
    auto lambda = createLambda();
    std::cout << lambda() << '\n';  // ❌ 소멸된 변수 접근 → 크래시
}

이 글에서 다루는 것:

  • 값 캡처 vs 참조 캡처
  • 댕글링 참조 방지
  • this 캡처
  • 초기화 캡처 (C++14)
  • 자주 나오는 람다 에러 10가지

목차

  1. 람다 캡처 기본
  2. 값 캡처 vs 참조 캡처
  3. 댕글링 참조 방지
  4. this 캡처
  5. 자주 나오는 에러 10가지
  6. 정리

1. 람다 캡처 기본

캡처 방식

int x = 10;
int y = 20;

// [=]: 모든 변수를 값으로 캡처
auto lambda1 = [=]() { return x + y; };

// [&]: 모든 변수를 참조로 캡처
auto lambda2 = [&]() { x = 30; return x + y; };

// [x]: x만 값으로 캡처
auto lambda3 = [x]() { return x * 2; };

// [&x]: x만 참조로 캡처
auto lambda4 = [&x]() { x = 30; };

// [x, &y]: x는 값, y는 참조
auto lambda5 = [x, &y]() { return x + y; };

// [=, &y]: 기본 값, y만 참조
auto lambda6 = [=, &y]() { y = 30; return x + y; };

mutable 람다

int x = 10;

// ❌ 값 캡처는 수정 불가
auto lambda1 = [x]() {
    // x = 20;  // 컴파일 에러: cannot assign to a variable captured by copy
};

// ✅ mutable로 수정 가능
auto lambda2 = [x]() mutable {
    x = 20;  // OK (람다 내부 복사본 수정)
    return x;
};

std::cout << lambda2() << '\n';  // 20
std::cout << x << '\n';  // 10 (원본은 변경 안 됨)

2. 값 캡처 vs 참조 캡처

값 캡처 [=]

int x = 10;

auto lambda = [=]() {  // x를 복사
    return x * 2;
};

x = 20;  // 원본 변경
std::cout << lambda() << '\n';  // 20 (캡처 시점의 값: 10)

특징:

  • 캡처 시점의 값을 복사
  • 원본 변경 영향 없음
  • 안전 (댕글링 참조 없음)

참조 캡처 [&]

int x = 10;

auto lambda = [&]() {  // x를 참조
    return x * 2;
};

x = 20;  // 원본 변경
std::cout << lambda() << '\n';  // 40 (현재 값: 20)

특징:

  • 원본을 참조
  • 원본 변경 즉시 반영
  • 위험 (원본 소멸 시 댕글링)

3. 댕글링 참조 방지

문제 코드

// ❌ 댕글링 참조
std::function<int()> createLambda() {
    int x = 42;
    return [&x]() { return x; };  // x를 참조 캡처
}  // x 소멸

int main() {
    auto lambda = createLambda();
    std::cout << lambda() << '\n';  // ❌ 소멸된 변수 접근
}

해결법 1: 값 캡처

// ✅ 값 캡처
std::function<int()> createLambda() {
    int x = 42;
    return [x]() { return x; };  // x를 복사
}  // x 소멸해도 람다는 복사본 보유

int main() {
    auto lambda = createLambda();
    std::cout << lambda() << '\n';  // 42 (안전)
}

해결법 2: shared_ptr

// ✅ shared_ptr로 수명 연장
std::function<int()> createLambda() {
    auto x = std::make_shared<int>(42);
    return [x]() { return *x; };  // shared_ptr 복사
}  // x의 참조 카운트 유지

int main() {
    auto lambda = createLambda();
    std::cout << lambda() << '\n';  // 42 (안전)
}

4. this 캡처

[this] vs [=] vs [*this]

class MyClass {
    int value_ = 42;
    
public:
    auto getLambda1() {
        return [this]() { return value_; };  // this 포인터 캡처
    }
    
    auto getLambda2() {
        return [=]() { return value_; };  // [=]는 암시적으로 this 캡처
    }
    
    auto getLambda3() {
        return [*this]() { return value_; };  // C++17: 객체 전체 복사
    }
};

MyClass obj;
auto lambda = obj.getLambda1();  // this 포인터 저장

// obj가 소멸되면 lambda는 댕글링!

안전한 패턴

class MyClass {
    int value_ = 42;
    
public:
    // ✅ 객체 전체 복사 (C++17)
    auto getLambda() {
        return [*this]() { return value_; };  // 객체 복사
    }
    
    // ✅ 필요한 멤버만 복사
    auto getLambda2() {
        int v = value_;
        return [v]() { return v; };  // 값만 복사
    }
};

5. 자주 나오는 에러 10가지

에러 1: 참조 캡처 후 변수 소멸

// ❌ 댕글링 참조
std::function<void()> func;

{
    int x = 42;
    func = [&x]() { std::cout << x << '\n'; };
}  // x 소멸

func();  // ❌ 크래시

에러 2: 값 캡처 수정 시도

// ❌ const 람다
int x = 10;
auto lambda = [x]() {
    x = 20;  // 컴파일 에러
};

// error: cannot assign to a variable captured by copy in a non-mutable lambda

// ✅ mutable 추가
auto lambda2 = [x]() mutable {
    x = 20;  // OK
};

에러 3: 멤버 변수 캡처 실수

class MyClass {
    int value_ = 42;
    
public:
    auto getLambda() {
        // ❌ value_를 직접 캡처 불가
        // return [value_]() { return value_; };  // 컴파일 에러
        
        // ✅ this 캡처 또는 복사
        return [this]() { return value_; };  // this 포인터
        
        // ✅ 또는 C++14 초기화 캡처
        return [v = value_]() { return v; };  // 값 복사
    }
};

에러 4: 초기화되지 않은 캡처

// ❌ 초기화 안 된 변수 캡처
int x;  // 초기화 안 함
auto lambda = [x]() { return x; };  // 쓰레기 값 캡처

// ✅ 초기화 후 캡처
int x = 42;
auto lambda = [x]() { return x; };

에러 5: std::move 캡처 실수

// ❌ 참조 캡처 후 move
std::unique_ptr<int> ptr = std::make_unique<int>(42);

auto lambda = [&ptr]() {  // 참조 캡처
    auto p = std::move(ptr);  // ptr을 이동
};

lambda();
std::cout << *ptr << '\n';  // ❌ ptr은 이제 nullptr

// ✅ 초기화 캡처로 이동 (C++14)
auto lambda2 = [ptr = std::move(ptr)]() {  // ptr을 람다로 이동
    // ...
};
// 원본 ptr은 이제 nullptr (의도된 동작)

에러 6: 임시 객체 참조 캡처

// ❌ 임시 객체 참조 캡처
auto lambda = [&s = std::string("Hello")]() {  // 임시 객체
    return s;
};  // 임시 객체 소멸

std::cout << lambda() << '\n';  // ❌ 댕글링 참조

// ✅ 값 캡처
auto lambda2 = [s = std::string("Hello")]() {  // 복사
    return s;
};

에러 7: 비동기 실행 시 참조 캡처

// ❌ 스레드에서 참조 캡처
void foo() {
    int x = 42;
    
    std::thread t([&x]() {  // x를 참조 캡처
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << x << '\n';  // ❌ x는 이미 소멸
    });
    
    t.detach();  // 스레드 분리
}  // x 소멸

// ✅ 값 캡처
void foo() {
    int x = 42;
    
    std::thread t([x]() {  // x를 복사
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << x << '\n';  // 안전
    });
    
    t.detach();
}

에러 8: 람다를 반환 시 참조 캡처

// ❌ 참조 캡처 후 반환
auto createLambda(int& x) {
    return [&x]() { return x; };  // x를 참조 캡처
}

int main() {
    int y = 42;
    auto lambda = createLambda(y);
    // y가 스코프를 벗어나면 lambda는 댕글링
}

// ✅ 값 캡처
auto createLambda(int x) {
    return [x]() { return x; };  // x를 복사
}

에러 9: 제네릭 람다 타입 추론 실수

// ❌ 타입 추론 실패
auto lambda =  {
    return x + y;
};

struct NoAdd {};
lambda(NoAdd{}, NoAdd{});  // 컴파일 에러: operator+ 없음

// error: invalid operands to binary expression ('NoAdd' and 'NoAdd')

// ✅ Concepts로 제약 (C++20)
auto lambda2 = []<typename T>(T x, T y) requires requires { x + y; } {
    return x + y;
};

에러 10: 캡처 리스트 오타

// ❌ 오타
int x = 10, y = 20;

auto lambda = [x, z]() {  // z는 없음
    return x + y;
};

// error: 'z' in capture list does not name a variable

실전 사례 분석

사례 1: 이벤트 핸들러

요구사항: 버튼 클릭 시 콜백 실행.

class Button {
    std::function<void()> onClick_;
    
public:
    void setOnClick(std::function<void()> callback) {
        onClick_ = callback;
    }
    
    void click() {
        if (onClick_) onClick_();
    }
};

// ❌ 댕글링 참조
void setupButton(Button& btn) {
    int counter = 0;
    
    btn.setOnClick([&counter]() {  // counter 참조 캡처
        ++counter;
        std::cout << "Clicked " << counter << " times\n";
    });
}  // counter 소멸 → 람다는 댕글링

// ✅ shared_ptr로 수명 연장
void setupButton(Button& btn) {
    auto counter = std::make_shared<int>(0);
    
    btn.setOnClick([counter]() {  // shared_ptr 복사
        ++(*counter);
        std::cout << "Clicked " << *counter << " times\n";
    });
}

사례 2: 비동기 작업

// ❌ 참조 캡처 (위험)
void processAsync(const std::vector<int>& data) {
    std::thread t([&data]() {  // data 참조 캡처
        for (int x : data) {
            process(x);
        }
    });
    t.detach();
}  // data 소멸 → 스레드는 댕글링 참조

// ✅ 값 캡처 (안전)
void processAsync(const std::vector<int>& data) {
    std::thread t([data]() {  // data 복사
        for (int x : data) {
            process(x);
        }
    });
    t.detach();
}

// ✅ 또는 shared_ptr
void processAsync(std::shared_ptr<std::vector<int>> data) {
    std::thread t([data]() {  // shared_ptr 복사
        for (int x : *data) {
            process(x);
        }
    });
    t.detach();
}

정리

람다 캡처 선택 가이드

상황권장 캡처이유
짧은 수명[=] 값안전
긴 수명[=] 값 또는 shared_ptr댕글링 방지
큰 객체[&] 참조 (수명 주의)복사 비용
멤버 변수[*this] (C++17)객체 복사
비동기[=] 값안전
즉시 실행[&] 참조빠름

캡처 에러 방지 체크리스트

  • 참조 캡처한 변수가 람다보다 오래 사는가?
  • 비동기 실행 시 값 캡처를 사용하는가?
  • 멤버 변수는 [*this]로 캡처하는가? (C++17)
  • mutable이 필요한가?
  • std::move 캡처는 초기화 캡처를 사용하는가?

핵심 규칙

  1. 기본은 값 캡처 [=] (안전)
  2. 참조 캡처는 수명 주의 (즉시 실행만)
  3. 비동기는 값 캡처 또는 shared_ptr
  4. *멤버 변수는 [this] (C++17)
  5. AddressSanitizer로 검증

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

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

  • C++ 람다 기초 | lambda 완벽 가이드
  • C++ 람다 고급 | 제네릭 람다·초기화 캡처
  • C++ 클로저 | 람다와 함수 객체
  • C++ std::function vs 템플릿 | 성능 비교

마치며

람다 캡처편리하지만 위험합니다. 특히 참조 캡처댕글링 참조를 일으키기 쉽습니다.

핵심 원칙:

  1. 기본은 값 캡처 (안전)
  2. 참조 캡처는 즉시 실행만
  3. 비동기는 값 캡처 또는 shared_ptr
  4. AddressSanitizer로 검증

람다를 저장하거나 비동기 실행할 때는 값 캡처를 사용하세요. 참조 캡처는 즉시 실행하는 STL 알고리즘에서만 안전합니다.

다음 단계: 람다를 이해했다면, C++ 함수 객체와 람다에서 더 깊이 배워보세요.


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |