enable_shared_from_this: this를 shared_ptr로 넘기는 법과 bad_weak_ptr 함정

이 글의 핵심

this를 그냥 shared_ptr로 감싸면 왜 이중 해제가 나는지, enable_shared_from_this가 이를 어떻게 해결하는지, 생성자 호출 문제와 비동기 콜백에서의 실전 패턴을 정리합니다.

들어가며: this를 비동기 작업에 넘기는 문제

비동기 작업이나 콜백에 객체 자신을 넘겨야 하는 경우가 많습니다. 그런데 this 포인터를 그대로 캡처하면, 작업이 실행되는 시점에 객체가 이미 소멸해 댕글링 포인터가 될 수 있습니다.

#include <iostream>
#include <thread>
#include <chrono>
// 위험한 코드
class Widget {
public:
    void startAsync() {
        std::thread([this]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            this->process();  // Widget이 이미 소멸됐을 수 있음
        }).detach();
    }

    void process() {
        std::cout << "Processing...\n";
    }
};

int main() {
    {
        Widget w;
        w.startAsync();
    }  // Widget 소멸

    std::this_thread::sleep_for(std::chrono::seconds(2));
    // 스레드가 소멸된 Widget에 접근 → 미정의 동작
}

이 글은 this를 shared_ptr로 바로 감싸면 왜 이중 삭제가 나는지에서 출발해, enable_shared_from_this가 내부의 weak_ptr로 이 문제를 푸는 방식, 생성자에서 호출하면 bad_weak_ptr이 나는 이유, C++17 weak_from_this(), 그리고 비동기 콜백과 Observer 패턴에서 쓰는 방법과 함정을 다룹니다.


문제: this를 shared_ptr로 감쌀 수 없다

잘못된 접근: shared_ptr(this)

// 하면 안 되는 코드
#include <memory>
#include <thread>
#include <iostream>

class Widget {
public:
    void startAsync() {
        // this로 새 shared_ptr 생성 - 이중 삭제
        auto self = std::shared_ptr<Widget>(this);

        std::thread([self]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            self->process();
        }).detach();
    }

    void process() {
        std::cout << "Processing...\n";
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
    widget->startAsync();

    std::this_thread::sleep_for(std::chrono::seconds(2));
}   // 이중 삭제 (double delete)

widget은 이미 make_shared가 만든 제어 블록으로 관리되는 객체입니다. shared_ptr<Widget>(this)는 그 사실을 알지 못하므로 새로운 제어 블록을 하나 더 만들고, 결과적으로 같은 객체를 서로 모르는 두 소유 그룹이 관리하게 됩니다. 스레드의 self가 사라지는 순간 두 번째 제어 블록의 참조 카운트가 0이 되어 객체가 삭제되고, main이 끝날 때 첫 번째 제어 블록이 같은 객체를 다시 삭제합니다.

제어 블록이 중복됨

widget (shared_ptr)
  ↓
제어 블록 1 (ref_count = 1)
  ↓
Widget 객체 (메모리)
  ↑
제어 블록 2 (ref_count = 1)  ← shared_ptr<Widget>(this)
  ↑
self (shared_ptr)

// self의 ref_count가 0 → Widget 삭제
// widget의 ref_count가 0 → Widget 삭제 (이미 삭제됨) → 미정의 동작

해결책: enable_shared_from_this

enable_shared_from_this 상속

std::enable_shared_from_this<T>를 상속하면 기존 제어 블록을 공유하는 shared_ptr을 돌려주는 shared_from_this()를 쓸 수 있습니다.

// enable_shared_from_this 상속
#include <memory>
#include <thread>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
public:
    void startAsync() {
        auto self = shared_from_this();  // 기존 제어 블록 공유

        std::thread([self]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            self->process();  // self가 객체 수명을 보장
        }).detach();
    }

    void process() {
        std::cout << "Processing...\n";
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
    widget->startAsync();

    std::this_thread::sleep_for(std::chrono::seconds(2));
}

내부 동작 원리

// enable_shared_from_this의 내부 구조 (개념적)
template <typename T>
class enable_shared_from_this {
private:
    mutable std::weak_ptr<T> weak_this_;  // 자신을 소유하는 제어 블록을 가리킴

public:
    std::shared_ptr<T> shared_from_this() {
        // weak_ptr → shared_ptr 변환. 만료 상태면 bad_weak_ptr 예외
        return std::shared_ptr<T>(weak_this_);
    }

    std::shared_ptr<const T> shared_from_this() const {
        return std::shared_ptr<const T>(weak_this_);
    }
};

// shared_ptr 생성자(make_shared 포함)가 weak_this_를 자동으로 설정
// auto widget = std::make_shared<Widget>();
// → widget의 weak_this_가 같은 제어 블록을 가리킴

제어 블록을 공유

widget (shared_ptr)
  ↓
제어 블록 (ref_count = 1, weak_count = 1)
  ↓
Widget 객체 (메모리)
  ↑ weak_this_ (weak_ptr)

// shared_from_this() 호출 후:
self (shared_ptr)  ← 같은 제어 블록 사용
  ↓
제어 블록 (ref_count = 2, weak_count = 1)

shared_from_this() 사용법

shared_from_this()가 항상 새 제어 블록을 만드는 std::shared_ptr<T>(this)와 다른 이유는, enable_shared_from_this를 상속한 클래스가 내부에 weak_ptr<T> 멤버를 하나 갖고 있고, std::make_shared나 shared_ptr 생성자가 객체를 소유하게 될 때 이 멤버를 그 제어 블록에 연결해 주기 때문입니다. 즉 shared_from_this()는 새로운 소유권을 만드는 것이 아니라 이미 있는 소유 그룹에 참조를 하나 더하는 연산이며, 그래서 use_count()가 1에서 2로 늘어납니다.

shared_from_this 사용 시 ref_count 변화

그림: 실제 실행 결과 — shared_from_this로 두 번째 shared_ptr을 얻으면 ref_count가 1에서 2가 되고, 마지막에 Node가 소멸한다.

기본 사용

#include <memory>
#include <iostream>

class Node : public std::enable_shared_from_this<Node> {
private:
    int value_;

public:
    explicit Node(int value) : value_(value) {
        std::cout << "Node(" << value_ << ") 생성\n";
    }

    ~Node() {
        std::cout << "Node(" << value_ << ") 소멸\n";
    }

    std::shared_ptr<Node> getShared() {
        return shared_from_this();
    }

    int getValue() const { return value_; }
};

int main() {
    auto node = std::make_shared<Node>(42);
    std::cout << "ref_count = " << node.use_count() << '\n';  // 1

    auto node2 = node->getShared();
    std::cout << "ref_count = " << node.use_count() << '\n';  // 2

    std::cout << "node2 value = " << node2->getValue() << '\n';
}

출력:

Node(42) 생성
ref_count = 1
ref_count = 2
node2 value = 42
Node(42) 소멸

비동기 작업에서 사용

#include <memory>
#include <thread>
#include <iostream>
#include <chrono>

class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
private:
    int id_;

public:
    explicit AsyncWorker(int id) : id_(id) {
        std::cout << "Worker " << id_ << " 생성\n";
    }

    ~AsyncWorker() {
        std::cout << "Worker " << id_ << " 소멸\n";
    }

    void startWork() {
        auto self = shared_from_this();

        std::thread([self]() {
            std::this_thread::sleep_for(std::chrono::seconds(2));
            self->doWork();
        }).detach();

        std::cout << "Worker " << id_ << " 작업 시작\n";
    }

private:
    void doWork() {
        std::cout << "Worker " << id_ << " 작업 완료\n";
    }
};

int main() {
    {
        auto worker = std::make_shared<AsyncWorker>(1);
        worker->startWork();
    }  // worker 변수는 사라지지만 스레드가 shared_ptr을 보유 중

    std::cout << "main: worker 스코프 벗어남\n";

    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "main: 종료\n";
}

출력:

Worker 1 생성
Worker 1 작업 시작
main: worker 스코프 벗어남
Worker 1 작업 완료
Worker 1 소멸
main: 종료

출력에서 주목할 부분은 순서입니다. worker 변수가 main의 중괄호를 벗어났는데도 “Worker 1 소멸”은 바로 출력되지 않고 “작업 완료” 다음에 출력됩니다. startWork() 안에서 만든 self가 스레드 람다에 캡처되어 있는 동안에는 참조 카운트가 0이 되지 않기 때문입니다. 마지막 소유자가 스레드 쪽의 self이므로 소멸자도 그 스레드에서 실행됩니다. self 대신 this를 캡처했다면 스레드가 깨어났을 때 객체는 이미 소멸했을 수 있고, this->doWork()는 미정의 동작이 됩니다.


생성자에서 호출 시 문제

bad_weak_ptr 예외

생성자 안에서 shared_from_this()를 호출하면 C++17부터는 std::bad_weak_ptr 예외가 발생합니다. C++14까지는 이 상황이 미정의 동작이었으므로, 오래된 표준으로 빌드하면 예외 대신 다른 증상이 나타날 수 있습니다.

// 생성자에서 shared_from_this() 호출
#include <memory>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
public:
    Widget() {
        try {
            auto self = shared_from_this();  // 예외 발생
        } catch (const std::bad_weak_ptr& e) {
            std::cout << "예외: " << e.what() << '\n';
        }
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
}

출력(메시지 문자열은 구현마다 다릅니다):

예외: bad_weak_ptr

왜 발생하는가?

auto widget = std::make_shared<Widget>();

// 1. 제어 블록과 객체를 담을 메모리 할당
// 2. Widget 생성자 실행     ← 이 시점에는 weak_this_가 비어 있음
// 3. shared_ptr이 객체를 소유하면서 weak_this_ 설정
// 4. widget에 shared_ptr 반환

weak_this_를 채우는 주체는 shared_ptr 쪽이고, 그 작업은 객체가 완전히 생성된 뒤에야 일어납니다. 생성자 안에서는 아직 비어 있는 weak_ptr을 shared_ptr로 바꾸려는 셈이라 예외가 납니다.

해결책 1: 팩토리 함수

// 팩토리 함수로 생성과 초기화를 묶기
#include <memory>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
private:
    Widget() {
        std::cout << "Widget 생성\n";
    }

public:
    static std::shared_ptr<Widget> create() {
        // 생성자가 private이라 make_shared는 접근할 수 없으므로 new 사용
        auto widget = std::shared_ptr<Widget>(new Widget());
        widget->initialize();  // 이 시점에는 weak_this_가 설정되어 있음
        return widget;
    }

private:
    void initialize() {
        auto self = shared_from_this();
        std::cout << "초기화 완료, ref_count = " << self.use_count() << '\n';
    }
};

int main() {
    auto widget = Widget::create();
}

출력:

Widget 생성
초기화 완료, ref_count = 2

생성자를 private으로 두면 스택이나 unique_ptr처럼 shared_ptr이 아닌 방식으로 객체를 만드는 실수를 컴파일 단계에서 막을 수 있습니다. 다만 생성자 안에서 shared_from_this()를 부르는 실수까지 막아 주지는 않으므로, shared_from_this()가 필요한 초기화는 initialize()처럼 별도 함수로 옮겨야 합니다. std::shared_ptr<Widget>(new Widget())은 객체와 제어 블록을 따로 할당하므로 make_shared보다 할당이 한 번 많습니다. 이것이 신경 쓰인다면 private 태그 타입을 public 생성자의 인자로 받는 passkey 기법으로 make_shared를 쓸 수 있습니다.

해결책 2: init() 메서드 분리

#include <iostream>
#include <memory>
// 초기화 메서드 분리
class Widget : public std::enable_shared_from_this<Widget> {
public:
    Widget() {
        std::cout << "Widget 생성\n";
    }

    // 생성 후 반드시 호출해야 함
    void init() {
        auto self = shared_from_this();
        startAsyncTasks();
    }

private:
    void startAsyncTasks() {
        std::cout << "비동기 작업 시작\n";
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
    widget->init();
}

이 방식은 간단하지만 init() 호출을 잊어도 컴파일러가 알려 주지 않습니다. 호출을 강제해야 한다면 해결책 1의 팩토리 함수가 더 안전합니다.


weak_from_this() (C++17)

weak_from_this() 소개

C++17부터는 weak_from_this()로 객체 수명을 늘리지 않는 weak_ptr을 얻을 수 있습니다. shared_from_this()와 달리 객체가 shared_ptr로 관리되지 않는 상황에서도 예외를 던지지 않고 빈 weak_ptr을 돌려줍니다.

// weak_from_this() (C++17)
#include <memory>
#include <iostream>

class Node : public std::enable_shared_from_this<Node> {
private:
    int value_;

public:
    explicit Node(int value) : value_(value) {}

    std::weak_ptr<Node> getWeak() {
        return weak_from_this();
    }

    void process() {
        std::cout << "Processing " << value_ << '\n';
    }
};

int main() {
    std::weak_ptr<Node> weakNode;

    {
        auto node = std::make_shared<Node>(42);
        weakNode = node->getWeak();

        if (auto shared = weakNode.lock()) {
            shared->process();
        }
    }  // node 소멸

    if (auto shared = weakNode.lock()) {
        shared->process();
    } else {
        std::cout << "Node가 이미 소멸됨\n";  // 이 경로
    }
}

출력:

Processing 42
Node가 이미 소멸됨

Observer 패턴에서 활용

// Subject가 Observer를 소유하지 않도록 weak_ptr로 보관
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include <iostream>

class Observer : public std::enable_shared_from_this<Observer> {
private:
    int id_;

public:
    explicit Observer(int id) : id_(id) {
        std::cout << "Observer " << id_ << " 생성\n";
    }

    ~Observer() {
        std::cout << "Observer " << id_ << " 소멸\n";
    }

    void notify(const std::string& event) {
        std::cout << "Observer " << id_ << " 이벤트: " << event << '\n';
    }

    std::weak_ptr<Observer> getWeak() {
        return weak_from_this();
    }
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;

public:
    void addObserver(std::weak_ptr<Observer> observer) {
        observers_.push_back(std::move(observer));
    }

    void notifyAll(const std::string& event) {
        // 소멸된 Observer 제거
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& weak) {
                    return weak.expired();
                }),
            observers_.end()
        );

        // 살아 있는 Observer에게 알림
        for (auto& weakObs : observers_) {
            if (auto obs = weakObs.lock()) {
                obs->notify(event);
            }
        }
    }
};

int main() {
    Subject subject;

    {
        auto obs1 = std::make_shared<Observer>(1);
        auto obs2 = std::make_shared<Observer>(2);

        subject.addObserver(obs1->getWeak());
        subject.addObserver(obs2->getWeak());

        subject.notifyAll("이벤트 1");
    }  // obs1, obs2 소멸

    std::cout << "\n소멸 후 알림:\n";
    subject.notifyAll("이벤트 2");  // 아무도 받지 않음
}

출력:

Observer 1 생성
Observer 2 생성
Observer 1 이벤트: 이벤트 1
Observer 2 이벤트: 이벤트 1
Observer 2 소멸
Observer 1 소멸

소멸 후 알림:

지역 변수는 선언의 역순으로 소멸하므로 obs2가 먼저 소멸합니다. expired()로 걸러낸 뒤에도 lock() 결과를 다시 확인하는 이유는, 멀티스레드 환경에서는 두 호출 사이에 다른 스레드가 마지막 shared_ptr을 놓을 수 있기 때문입니다.


실전 패턴: 비동기 작업

콜백을 받는 비동기 요청과 반복 실행되는 타이머는 객체를 만든 시점과 콜백이 실제로 실행되는 시점 사이에 시간차가 있고, 그 사이 원본 객체의 수명이 호출자의 손을 떠난다는 공통점이 있습니다. 이때 콜백이 객체 수명을 연장해야 하는지(shared_from_this), 아니면 객체가 살아 있을 때만 실행되면 되는지(weak_from_this)를 먼저 정해야 합니다.

HTTP 클라이언트: 요청이 끝날 때까지 객체를 살려 두기

// 비동기 HTTP 클라이언트 (네트워크는 sleep으로 흉내)
#include <memory>
#include <thread>
#include <iostream>
#include <functional>
#include <chrono>
#include <string>

class HttpClient : public std::enable_shared_from_this<HttpClient> {
private:
    std::string url_;

public:
    explicit HttpClient(const std::string& url) : url_(url) {}

    using ResponseCallback = std::function<void(const std::string&)>;

    void getAsync(ResponseCallback callback) {
        auto self = shared_from_this();

        std::thread([self, callback]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::string response = "Response from " + self->url_;
            callback(response);
        }).detach();
    }
};

int main() {
    {
        auto client = std::make_shared<HttpClient>("https://api.example.com");

        client->getAsync([](const std::string& response) {
            std::cout << "콜백: " << response << '\n';
        });

        std::cout << "요청 전송됨\n";
    }  // client 변수는 사라지지만 스레드가 소유권을 보유

    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "main 종료\n";
}

출력:

요청 전송됨
콜백: Response from https://api.example.com
main 종료

client 변수가 스코프를 벗어나도 콜백이 정상적으로 실행되는 것은 self가 스레드 람다에 캡처되어 HttpClient의 소유권을 하나 더 쥐고 있기 때문입니다. self 대신 this를 캡처했다면 “요청 전송됨” 출력 직후 client가 소멸하고, 1초 뒤 스레드가 this->url_에 접근하는 순간 미정의 동작이 됩니다. 크래시로 드러나면 그나마 찾기 쉽지만, 해제된 메모리가 재사용되어 엉뚱한 값을 조용히 읽는 경우가 더 디버깅하기 어렵습니다.

타이머: 소유자가 사라지면 멈추기

타이머에 같은 방식으로 self를 캡처하면 문제가 생깁니다. 스레드가 shared_ptr을 쥐고 있는 한 참조 카운트가 0이 되지 않으므로, 소유자가 타이머를 놓아도 소멸자가 호출되지 않고 타이머는 정해진 횟수를 다 채울 때까지 계속 돌아갑니다. 소멸자에서 정지 플래그를 세우는 코드는 이 경우 아무 효과가 없습니다. 소유자가 사라지면 타이머도 멈춰야 한다면, 스레드에는 weak_ptr만 넘기고 매 틱마다 lock()으로 살아 있는지 확인합니다.

// weak_ptr로 캡처해 소유자가 사라지면 멈추는 타이머
#include <memory>
#include <thread>
#include <iostream>
#include <chrono>
#include <atomic>

class Timer : public std::enable_shared_from_this<Timer> {
private:
    int id_;
    std::atomic<bool> stopped_{false};

public:
    explicit Timer(int id) : id_(id) {}

    ~Timer() {
        std::cout << "Timer " << id_ << " 소멸\n";
    }

    void start(int intervalMs, int repeatCount) {
        std::weak_ptr<Timer> weak = weak_from_this();

        std::thread([weak, intervalMs, repeatCount]() {
            for (int i = 0; i < repeatCount; ++i) {
                std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
                auto self = weak.lock();  // 틱마다 살아 있는지 확인
                if (!self || self->stopped_) return;
                self->onTimer(i + 1);
            }   // self는 반복마다 해제되므로 sleep 중에는 소유권을 쥐지 않음
        }).detach();
    }

    void stop() { stopped_ = true; }

private:
    void onTimer(int tick) {
        std::cout << "Timer " << id_ << " tick " << tick << '\n';
    }
};

int main() {
    {
        auto timer = std::make_shared<Timer>(1);
        timer->start(500, 5);  // 500ms마다 최대 5번

        std::this_thread::sleep_for(std::chrono::milliseconds(1200));
    }  // 마지막 소유자가 사라져 Timer 소멸, 스레드는 다음 틱에서 종료

    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "main 종료\n";
}

출력:

Timer 1 tick 1
Timer 1 tick 2
Timer 1 소멸
main 종료

lock()에 성공한 동안에는 그 틱이 끝날 때까지 객체가 살아 있으므로 onTimer 실행 도중에 객체가 사라질 걱정은 없습니다. 반대로 틱 처리 중에 소유자가 타이머를 놓으면 스레드 쪽 self가 마지막 소유자가 되어 소멸자가 타이머 스레드에서 실행될 수 있다는 점은 알아 두어야 합니다. 소유자가 살아 있는 상태에서 명시적으로 멈추고 싶을 때는 stop()을 호출합니다. 메모리 수명(shared_ptr/weak_ptr)과 애플리케이션 로직상의 수명(stopped_ 플래그)은 별개의 문제이므로 둘 다 설계해야 합니다.


주의사항과 함정

shared_ptr로 관리되지 않는 객체에서 호출

weak_this_는 객체가 shared_ptr의 소유가 될 때만 설정됩니다. 스택에 선언한 객체나 unique_ptr로 만든 객체는 그 경로를 거치지 않으므로 weak_this_가 계속 비어 있고, 이때 shared_from_this()를 호출하면 생성자 문제와 같은 bad_weak_ptr 예외(C++17 이상)가 납니다.

// 스택 객체에서 shared_from_this() 호출
#include <memory>

class Widget : public std::enable_shared_from_this<Widget> {
public:
    void test() {
        auto self = shared_from_this();
    }
};

int main() {
    Widget widget;   // 스택 객체 (shared_ptr 아님)
    widget.test();   // bad_weak_ptr 예외
}

shared_from_this()를 쓰는 클래스는 shared_ptr로만 생성되도록 앞의 팩토리 함수 방식으로 생성 경로를 제한하는 것이 안전합니다.

다중 상속 시 모호성

Derived가 Base1과 Base2를 둘 다 상속하고 각 베이스가 enable_shared_from_this<Base1>, enable_shared_from_this<Base2>를 상속하면, Derived 객체에는 weak_this_가 두 개 생깁니다. Derived에서 shared_from_this()를 호출하면 어느 쪽인지 정할 수 없어 컴파일 에러가 납니다. Base1::shared_from_this()처럼 한정해서 호출해도 해결되지 않습니다. 표준은 shared_ptr이 모호하지 않은 enable_shared_from_this 베이스가 있을 때만 weak_this_를 설정하도록 정하고 있어서, 이 구조에서는 어느 쪽도 설정되지 않고 실행 중에 bad_weak_ptr이 납니다.

// 다중 enable_shared_from_this 상속
class Base1 : public std::enable_shared_from_this<Base1> {};
class Base2 : public std::enable_shared_from_this<Base2> {};

class Derived : public Base1, public Base2 {
    // shared_from_this()가 모호하고, weak_this_도 설정되지 않음
};

상속 계층에서 enable_shared_from_this는 한 곳에서만 상속해야 합니다.

// 한 번만 상속
class Base {};
class Derived : public Base,
                public std::enable_shared_from_this<Derived> {
};

공통 베이스가 enable_shared_from_this<Base>를 상속한 구조라면, 파생 클래스에서는 std::static_pointer_cast<Derived>(shared_from_this())로 파생 타입의 shared_ptr을 얻는 방법을 많이 씁니다.

소멸자에서 shared_from_this() 호출

소멸자가 실행된다는 것은 참조 카운트가 이미 0이 되었다는 뜻이므로, weak_this_는 만료 상태입니다. 이때 shared_from_this()를 호출하면 만료된 weak_ptr로 shared_ptr을 만들려는 셈이라 bad_weak_ptr 예외가 납니다. 소멸자에서 예외가 빠져나가면 기본적으로 noexcept인 소멸자 규칙 때문에 std::terminate가 호출됩니다. weak_from_this()는 예외 없이 만료된 weak_ptr을 돌려주지만, 이것으로 할 수 있는 일은 없습니다. 객체 자신을 다른 곳에 넘겨야 하는 정리 작업은 소멸자가 아니라 명시적인 close()나 shutdown() 메서드로 옮겨, 객체가 확실히 살아 있는 동안 실행되도록 합니다.

class Widget : public std::enable_shared_from_this<Widget> {
public:
    ~Widget() {
        // auto self = shared_from_this();  // bad_weak_ptr → std::terminate
    }
};

순환 참조

shared_from_this()는 자기 자신에 대한 소유 참조를 다른 객체에 넘기기 쉽게 만들기 때문에 순환 참조를 만들기도 쉽습니다. 노드 A가 B를 shared_ptr로 가리키고 B도 A를 shared_ptr로 가리키면, 외부 참조가 모두 사라져도 두 객체의 참조 카운트는 서로를 붙잡고 있어 0이 되지 않고 메모리가 해제되지 않습니다. 해법은 소유권이 한 방향으로만 흐르게 하고, 반대 방향은 weak_ptr로 두는 것입니다.

// 순환 참조 발생
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;

    void setNext(std::shared_ptr<Node> n) {
        next = n;
        n->prev = shared_from_this();  // 서로를 소유
    }
};

// 앞 방향은 소유, 뒤 방향은 weak_ptr
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;

    void setNext(std::shared_ptr<Node> n) {
        next = n;
        n->prev = weak_from_this();  // 수명에 영향 없음
    }
};

트리 구조에서도 같은 원리를 적용합니다. 부모가 자식을 shared_ptr로 소유하고, 자식은 부모를 weak_ptr로 참조합니다.

class Child;

class Parent : public std::enable_shared_from_this<Parent> {
public:
    std::vector<std::shared_ptr<Child>> children;  // 부모가 자식을 소유

    void addChild(std::shared_ptr<Child> child);
};

class Child {
public:
    std::weak_ptr<Parent> parent;  // 자식은 부모를 참조만
};

void Parent::addChild(std::shared_ptr<Child> child) {
    child->parent = weak_from_this();
    children.push_back(std::move(child));
}

자식에서 부모에 접근할 때는 parent.lock()의 결과가 비어 있는지 먼저 확인해야 합니다.

람다 캡처는 명시적으로

[this]로 캡처하면 컴파일은 되지만 객체 수명을 보장하지 못합니다. auto self = shared_from_this();를 먼저 만들고 [self]로 캡처하면, 코드를 읽는 사람이 이 람다가 객체 수명을 어떻게 다루는지 바로 알 수 있습니다. C++20에서는 [=]가 this를 암묵적으로 캡처하는 동작이 deprecated되었으므로, 기본 캡처에 기대지 말고 캡처 대상을 명시하는 편이 좋습니다.

참고 자료


같이 보면 좋은 글