C++ 람다 캡처로 생기는 dangling reference: 값 캡처·shared_ptr·[*this]로 수명 지키기

이 글의 핵심

람다 캡처 에러의 대부분은 캡처가 실제로 무엇을 만드는지 모르는 데서 옵니다. 값 캡처는 클로저 객체의 멤버 복사본이고, 참조 캡처는 수명을 늘리지 않는 참조이며, [=]는 멤버가 아니라 this를 잡습니다. 이 글은 참조 캡처 댕글링, this 수명, mutable 상태가 복사본마다 따로 노는 문제 등 자주 나오는 캡처 에러 10가지와 해결법을, GCC로 확인한 클로저 구조와 함께 정리합니다.

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

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가지

람다 캡처 기본

캡처 방식

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 (원본은 변경 안 됨)

값 캡처 vs 참조 캡처

값 캡처 [=]

int x = 10;

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

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

특징:

  • 캡처 시점의 값을 복사
  • 원본 변경 영향 없음
  • 캡처한 변수 자체는 안전 (단, 포인터나 this를 값으로 캡처하면 가리키는 대상의 수명은 여전히 문제)

“값 캡처는 안전하다”는 말에는 단서가 붙습니다. int* p를 값으로 캡처하면 복사되는 것은 포인터 값일 뿐이고, std::string_view나 반복자(iterator)를 값으로 캡처해도 원본 문자열이나 컨테이너가 사라지면 똑같이 댕글링입니다. 값 캡처가 수명 문제를 해결하는 것은 캡처한 타입이 자기 데이터를 소유할 때(int, std::string, std::vector, shared_ptr 등)뿐입니다.

참조 캡처 [&]

int x = 10;

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

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

특징:

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

댕글링 참조 방지

문제 코드

// ❌ 댕글링 참조
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 (안전)
}

shared_ptr 캡처는 값이 여러 람다 사이에서 공유되어야 할 때 쓰는 방법입니다. 단순히 수명만 늘리려는 목적이라면 해결법 1의 값 캡처가 더 가볍습니다. 또 shared_ptr 캡처는 순환 참조를 만들기 쉽다는 함정이 있습니다. 객체가 자신의 멤버 std::function에 shared_from_this()를 캡처한 람다를 저장하면, 객체가 람다를 소유하고 람다가 객체를 소유하므로 참조 카운트가 영원히 0이 되지 않아 메모리 누수가 됩니다. 이런 경우에는 std::weak_ptr를 캡처하고 람다 안에서 if (auto self = weak.lock()) { ... }로 객체가 아직 살아 있을 때만 실행하는 패턴을 씁니다. 이 패턴은 “객체가 이미 사라졌다면 콜백을 조용히 무시한다”는 의미도 함께 표현해 줍니다.


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는 댕글링!

세 방식의 차이는 “람다가 무엇을 들고 있는가”입니다. [this]와 [=]는 둘 다 포인터 하나만 들고 있으므로 람다 안에서 value_를 읽거나 수정하면 원본 객체에 그대로 반영되고, 원본이 사라지면 댕글링이 됩니다. 이 때문에 [=]를 “모든 것을 복사한다”고 이해한 사람에게는 멤버 수정이 원본에 반영되는 동작이 의외로 느껴집니다. C++20에서는 이 혼동을 줄이려고 [=]를 통한 암시적 this 캡처를 deprecated로 지정했고, GCC는 warning: implicit capture of 'this' via '[=]' is deprecated in C++20 [-Wdeprecated]를 냅니다. [*this]는 객체 전체를 복사하므로 원본과 독립적이지만, 객체가 크거나 복사할 수 없는 멤버(std::unique_ptr, std::mutex 등)가 있으면 비용이 크거나 컴파일되지 않습니다. 또 [*this] 람다의 operator()도 const라서 복사본의 멤버를 고치려면 mutable이 필요합니다.

안전한 패턴

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

자주 나오는 에러 10가지

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

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

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

func();  // ❌ 크래시

“크래시”라고 적었지만 실제로는 정의되지 않은 동작이라 매번 크래시가 나지는 않습니다. x가 있던 스택 자리가 아직 덮어써지지 않았다면 42가 멀쩡히 출력되고, 디버그 빌드에서는 늘 잘 되다가 -O2 릴리스 빌드나 다른 함수 호출이 끼어든 뒤에만 쓰레기 값이 나오기도 합니다. 제가 이 유형의 버그를 추적할 때 가장 곤란했던 점도 “테스트에서는 통과하는데 가끔 이상한 값이 찍힌다”는 형태로만 나타난다는 것이었습니다. 이럴 때는 추측하지 말고 -fsanitize=address로 빌드해서 실행해 보는 것이 가장 빠릅니다. AddressSanitizer는 스코프를 벗어난 스택 변수 접근을 “ERROR: AddressSanitizer: stack-use-after-scope”로, 반환된 함수의 스택 접근을 “stack-use-after-return”(환경에 따라 별도 옵션 필요)으로 보고하면서 람다 본문의 정확한 줄을 알려 줍니다. Clang의 -Wdangling, GCC 13 이상의 -Wdangling-reference 같은 경고도 일부 경우를 컴파일 시점에 잡아 주지만, 람다 캡처 댕글링은 대부분 경고 없이 통과하므로 런타임 도구가 필요합니다.

에러 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; };  // 값 복사
    }
};

캡처 목록에는 지역 변수(자동 저장 기간을 가진 변수)의 이름만 올 수 있어서, 멤버 이름을 넣으면 GCC는 error: capture of non-variable 'MyClass::value_'를 냅니다. 원래 예제처럼 두 return을 한 함수에 모두 남기면 두 람다의 타입이 서로 달라 inconsistent deduction for auto return type 에러가 나므로, 실제로는 둘 중 하나만 고릅니다. 초기화 캡처 [v = value_]는 람다를 만드는 순간의 값을 복사해 두므로 객체 수명과 무관해지지만, 이후 객체의 value_가 바뀌어도 람다는 옛 값을 보고 있다는 점을 의도와 맞춰 확인해야 합니다.

에러 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; };
// error: cannot bind non-const lvalue reference of type 'std::string&' to an rvalue

// ❌ 실제로 댕글링이 나는 형태: const 참조 매개변수를 통한 임시 객체
auto makeGreeter(const std::string& name) {
    return [&name]() { return "Hello, " + name; };  // 매개변수(=임시 객체)를 참조
}

auto greet = makeGreeter("Kim");  // "Kim" → 임시 std::string, 이 문장 끝에서 소멸
std::cout << greet() << '\n';     // ❌ 댕글링 참조

// ✅ 값 캡처
auto makeGreeter2(std::string name) {
    return [name = std::move(name)]() { return "Hello, " + name; };
}

[&s = std::string("Hello")]처럼 초기화 캡처에서 곧바로 임시 객체를 참조로 잡으려 하면, 비-const 좌값 참조는 임시 객체에 바인딩될 수 없으므로 컴파일러가 막아 줍니다. 실제로 문제가 되는 것은 두 번째 형태입니다. const std::string& 매개변수에 문자열 리터럴을 넘기면 호출하는 쪽에서 임시 std::string이 만들어지고, 이 임시 객체는 호출이 포함된 전체 문장이 끝날 때 소멸합니다. 함수 안에서 보기에는 평범한 매개변수라서 [&name]이 안전해 보이지만, 반환된 람다는 이미 사라진 임시 객체를 가리킵니다. const 참조가 임시 객체의 수명을 늘려 주는 규칙은 지역 변수를 직접 바인딩할 때만 적용되고, 매개변수나 멤버를 거치면 적용되지 않는다는 점이 이 버그의 핵심입니다.

에러 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();
}

detach()가 문제를 키우는 이유는 스레드와 호출자 사이의 수명 연결을 완전히 끊기 때문입니다. join()을 호출하면 foo()가 스레드 종료를 기다리므로 참조 캡처도 안전하지만, detach()한 스레드는 foo()가 반환된 뒤에도 계속 돌아갑니다. 값 캡처로 바꾼 두 번째 버전도 x는 안전해졌지만, 스레드가 끝나기 전에 main이 반환되면 전역 객체(std::cout 포함)가 파괴되는 중에 스레드가 접근하는 또 다른 수명 문제가 남습니다. 가능하면 detach()보다 std::jthread(C++20)나 std::async가 반환하는 future로 소유권을 명확히 하는 편이 좋습니다. 콜백 기반 비동기 라이브러리(Boost.Asio 등)에서도 같은 원리로, 핸들러가 참조하는 객체는 shared_from_this()로 얻은 shared_ptr을 캡처해 핸들러가 끝날 때까지 살려 두는 패턴이 표준처럼 쓰입니다.

C++20 코루틴을 쓴다면 한 가지 더 조심해야 합니다. 캡처가 있는 람다를 코루틴으로 만들면(람다 본문에 co_await가 있으면), 캡처한 값은 코루틴 프레임이 아니라 람다 객체(클로저)에 들어 있습니다. [x]() -> task<void> { co_await something(); use(x); }()처럼 임시 람다를 만들어 곧바로 호출하면, 첫 co_await에서 제어가 돌아온 뒤 임시 람다 객체는 소멸하고, 재개된 코루틴은 사라진 클로저의 x를 읽습니다. 값 캡처인데도 댕글링이 되는 드문 경우라서 원인을 찾기 어렵습니다. C++ Core Guidelines(CP.51)도 “캡처가 있는 람다를 코루틴으로 쓰지 말라”고 권하며, 필요한 값은 람다의 매개변수로 넘기면 코루틴 프레임에 복사되어 안전합니다.

에러 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 = [](auto x, auto y) {
    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;
};

엄밀히 말하면 이것은 “타입 추론 실패”가 아니라, 추론은 성공했는데 본문을 인스턴스화하는 단계에서 operator+를 찾지 못한 에러입니다. 제네릭 람다는 호출될 때마다 템플릿처럼 본문이 생성되므로, 에러 메시지가 람다 본문 안쪽을 가리키고, 람다가 여러 겹의 템플릿 안에서 호출되면 메시지가 수십 줄로 길어집니다. requires 절을 붙인 버전도 NoAdd로 호출하면 여전히 컴파일 에러이지만, “제약 조건을 만족하지 않는다(constraints not satisfied)“는 메시지가 호출 위치에서 나오므로 원인을 훨씬 빨리 찾을 수 있습니다. 또 <typename T> 형태는 두 인자가 같은 타입이어야 하므로 lambda2(1, 2.0)은 추론 단계에서 실패한다는 점도 원래 auto x, auto 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

캡처가 실제로 만드는 것: 클로저 멤버

위 에러들은 “람다 = 캡처한 변수를 멤버로 가진 이름 없는 클래스의 객체”라는 모델 하나로 거의 다 설명됩니다. 컴파일러는 [a, d] { return a + d; }를 대략 이렇게 바꿉니다.

class __lambda {
    int a; double d;                                  // 값 캡처 → 복사된 멤버
public:
    __lambda(int a, double d) : a(a), d(d) {}
    auto operator()() const { return a + d; }         // 기본은 const 멤버 함수
};

GCC 10(-O2)에서 크기를 재 보면 이 모델이 그대로 보입니다.

int a = 1; double d = 2;
auto byVal = [a, d]   { return a + d; };   // sizeof 16: int + 패딩 + double
auto byRef = [&a, &d] { return a + d; };   // sizeof 16: 참조 두 개(내부적으로 포인터)
auto none  = []       { return g; };       // sizeof 1: 멤버 없음 (g는 전역)

이 모델에서 곧바로 나오는 결론이 네 가지 있습니다.

  • 참조 캡처는 포인터를 들고 있을 뿐이라 원본의 수명을 늘리지 않습니다. 에러 1·6·7·8의 댕글링이 모두 여기서 나옵니다.
  • 전역·정적 변수는 캡처되지 않습니다. 위 none처럼 캡처 목록 없이 바로 접근하므로, 람다를 만든 뒤 g = 5;로 바꾸면 람다 안에서도 5가 보입니다. [=]로 “값을 고정했다”고 생각했던 전역 설정값이 나중에 바뀌어 있는 버그의 원인입니다. 고정하려면 [g = g]처럼 초기화 캡처로 복사합니다.
  • operator()가 기본으로 const 라서 값 캡처한 멤버를 고칠 수 없고(에러 2), mutable은 이 const를 떼는 것뿐입니다.
  • mutable 상태는 람다 객체마다 따로 있습니다. 람다를 복사하면 멤버도 복사되기 때문입니다.
auto counter = [n = 0]() mutable { return ++n; };
std::function<int()> f = counter;   // 이 시점의 counter가 복사됨
counter(); counter();
counter();   // 3
f();         // 1  ← 같은 카운터가 아니다

int calls = 0;
auto count = [calls](int) mutable { ++calls; };
std::for_each(v.begin(), v.end(), count);   // 알고리즘은 복사본을 받는다
// calls는 여전히 0, count 안의 calls도 0 (for_each가 가진 복사본만 3)

std::function에 넣거나, 알고리즘에 넘기거나, 다른 스레드로 보낼 때마다 복사가 일어나므로 “한 개의 카운터를 여러 곳에서 올린다”는 기대는 깨집니다(위 값은 GCC 10에서 실행한 결과). 공유해야 하는 상태라면 람다 멤버가 아니라 바깥 변수를 참조로 잡거나(수명 주의), std::shared_ptr로 감싸서 캡처하고, 알고리즘의 결과가 필요하면 std::for_each가 반환하는 함수 객체를 받아 씁니다.

실전 사례 분석

사례 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();
}

오래 보관하는 람다의 캡처를 고르는 기준

참조 캡처가 안전한 것은 람다가 캡처한 변수보다 먼저 끝나는 경우, 즉 std::sort나 std::find_if에 넘겨 그 자리에서 호출이 끝나는 경우입니다. 콜백으로 저장하거나 스레드·비동기 작업에 넘기는 람다는 값으로 잡고, 큰 객체라면 [v = std::move(bigVector)]처럼 초기화 캡처로 이동시키면 복사 비용과 댕글링을 동시에 피할 수 있습니다. [=]·[&] 같은 기본 캡처는 무엇이 잡혔는지 코드만 보고 알기 어려워서, C++ Core Guidelines도 비지역적으로 쓰일 람다에는 참조 캡처를 피하고(F.53) this나 멤버를 쓰는 람다에는 [=] 기본 캡처를 쓰지 말라고(F.54) 권합니다. 오래 보관되는 람다는 [id, name]처럼 필요한 변수만 명시해 두면 리뷰에서 수명을 따지기도 쉽습니다.

저장용 람다를 std::function에 넣으면 타입 소거 때문에 호출이 간접 호출이 되고, 캡처가 커지면 힙 할당이 생길 수 있습니다. 호출 빈도가 높은 곳에서는 template <typename F> void run(F&& f)처럼 템플릿으로 받아 인라인 여지를 남기는 것이 일반적인 선택입니다.


같이 보면 좋은 글

자주 묻는 질문 (FAQ)

Q. C++20에서 [=]로 멤버를 쓰는 람다에 경고가 뜨는 이유는 무엇인가요?

A. [=]는 멤버 변수를 값으로 복사하는 것처럼 보이지만, 실제로는 this 포인터를 암시적으로 캡처해서 멤버에 접근합니다. 그래서 람다가 객체보다 오래 살아남으면 값 캡처를 했다고 생각했는데도 댕글링 포인터 문제가 생기며, C++20에서는 이 암시적 this 캡처가 deprecated되어 경고가 납니다. 포인터 캡처가 의도라면 [=, this]로 명시하고, 객체 복사본이 필요하면 C++17의 [*this]를 쓰면 됩니다.