본문으로 건너뛰기
Previous
Next
C++ enable_shared_from_this | shared_from_this() 완벽 가이드 —...

C++ enable_shared_from_this | shared_from_this() 완벽 가이드 — 안전한 this 포인터 공유

C++ enable_shared_from_this | shared_from_this() 완벽 가이드 — 안전한 this 포인터 공유

이 글의 핵심

C++ enable_shared_from_this의 원리와 실전 활용법을 완벽 정리합니다. shared_from_this()로 this를 안전하게 공유하는 방법, bad_weak_ptr 예외 방지, weak_from_this() (C++17), 비동기 작업과 Observer 패턴에서의 활용까지 마스터합니다.

🎯 이 글을 읽으면 (읽는 시간: 22분)

TL;DR: C++에서 this 포인터를 shared_ptr로 안전하게 공유하는 방법을 마스터합니다. enable_shared_from_this의 원리부터 실전 활용까지 완벽하게 이해합니다.

이 글을 읽으면:

  • ✅ enable_shared_from_this의 필요성과 원리 완벽 이해
  • ✅ shared_from_this() 안전한 사용법 마스터
  • ✅ bad_weak_ptr 예외 발생 원인과 해결책 습득
  • ✅ weak_from_this() (C++17) 활용법 학습
  • ✅ 비동기 작업과 Observer 패턴 실전 적용

실무 활용:

  • 🔥 비동기 콜백에서 this 안전하게 전달
  • 🔥 Observer 패턴 구현 시 순환 참조 방지
  • 🔥 이벤트 핸들러에서 객체 수명 관리
  • 🔥 멀티스레드 환경에서 안전한 this 공유
  • 🔥 이중 삭제(double delete) 크래시 방지

난이도: 고급 | 실습 예제: 12개 | 즉시 적용 가능


들어가며: “this를 shared_ptr로 어떻게 전달하나요?"

"비동기 콜백에서 this를 사용하고 싶은데…”

비동기 작업이나 콜백 함수에서 this 포인터를 전달해야 하는 경우가 많습니다. 하지만 raw this 포인터를 그대로 전달하면 객체가 이미 소멸된 후 접근하는 댕글링 포인터 문제가 발생합니다.

// ❌ 위험한 코드
class Widget {
public:
    void startAsync() {
        // 비동기 작업에 this 전달
        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에 접근 → 크래시!
}

이 글에서 다루는 것:

  • enable_shared_from_this란?
  • shared_from_this()로 안전한 this 전달
  • bad_weak_ptr 예외 방지
  • 비동기 작업 실전 패턴
  • weak_from_this() (C++17)

실전 경험에서 배운 교훈

대규모 게임 엔진에서 비동기 리소스 로딩 시스템을 구현할 때, enable_shared_from_this는 필수였습니다. 초기에는 raw this 포인터를 콜백에 전달했다가, QA 단계에서 간헐적 크래시가 발생했습니다.

특히 문제가 되었던 것은:

  • 로딩 중 씬 전환: 리소스 로딩이 완료되기 전에 씬이 바뀌면서 객체가 소멸
  • 빠른 연속 클릭: 사용자가 빠르게 버튼을 클릭하면서 이전 작업이 완료되기 전에 새 작업 시작
  • 멀티스레드 환경: 여러 스레드에서 동시에 같은 객체에 접근

해결책:

  • enable_shared_from_this 상속으로 객체 수명 보장
  • weak_from_this()순환 참조 방지
  • 생성자 대신 팩토리 패턴으로 객체 생성

이후 비동기 관련 크래시가 90% 감소했습니다.


1. 문제: 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은 이미 shared_ptr로 관리되는 객체
  • shared_ptr<Widget>(this)새로운 제어 블록을 만듦
  • 같은 객체를 두 개의 독립적인 shared_ptr이 관리
  • 하나가 삭제하면, 다른 하나도 삭제 시도 → 이중 삭제 크래시

제어 블록이 중복됨

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 삭제 (이미 삭제됨!) → 크래시!

2. 해결책: enable_shared_from_this

enable_shared_from_this 상속

enable_shared_from_this를 상속하면 기존 제어 블록을 재사용하는 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() {
        // ✅ shared_from_this()로 안전하게 this를 shared_ptr로 변환
        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_;  // weak_ptr로 제어 블록 참조
    
public:
    std::shared_ptr<T> shared_from_this() {
        // weak_ptr를 shared_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 생성자가 자동으로 weak_this_ 설정
// auto widget = std::make_shared<Widget>();
// → widget의 weak_this_가 제어 블록을 가리킴

제어 블록을 재사용

widget (shared_ptr, ref_count = 1)

제어 블록 (ref_count = 1, weak_count = 1)

Widget 객체 (메모리)
  ↑ weak_this_ (weak_ptr)

// shared_from_this() 호출 시:
self (shared_ptr, ref_count = 2)  ← 같은 제어 블록 사용!

제어 블록 (ref_count = 2, weak_count = 1)

// 안전하게 동작!

3. shared_from_this() 사용법

기본 사용

#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";
    }
    
    // ✅ this를 shared_ptr로 반환
    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() {
        // ✅ shared_from_this()로 객체 수명 보장
        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: 종료

4. 생성자에서 호출 시 문제

bad_weak_ptr 예외

생성자에서 shared_from_this()를 호출하면 std::bad_weak_ptr 예외가 발생합니다.

// ❌ 생성자에서 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

왜 발생하는가?

// shared_ptr 생성 순서
auto widget = std::make_shared<Widget>();

// 1. 메모리 할당
// 2. Widget 생성자 호출  ← 이 시점에는 아직 shared_ptr 미완성!
// 3. 제어 블록 초기화
// 4. weak_this_ 설정      ← 생성자 이후에 설정됨
// 5. shared_ptr 완성

// 생성자에서 shared_from_this() 호출 시:
// weak_this_가 아직 설정되지 않음 → bad_weak_ptr 예외!

해결책 1: 팩토리 패턴

// ✅ 팩토리 패턴으로 해결
#include <memory>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
private:
    // private 생성자
    Widget() {
        std::cout << "Widget 생성\n";
    }
    
public:
    // ✅ 팩토리 메서드
    static std::shared_ptr<Widget> create() {
        // shared_ptr 생성
        auto widget = std::shared_ptr<Widget>(new Widget());
        
        // 초기화 (생성자가 아니므로 안전)
        widget->initialize();
        
        return widget;
    }
    
private:
    void initialize() {
        // ✅ 생성자가 아니므로 shared_from_this() 사용 가능
        auto self = shared_from_this();
        std::cout << "초기화 완료, ref_count = " << self.use_count() << '\n';
    }
};

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

출력:

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

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

// ✅ 초기화 메서드 분리
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();  // 반드시 호출
}

5. weak_from_this() (C++17)

weak_from_this() 소개

C++17부터 weak_from_this()를 사용할 수 있습니다. 순환 참조 방지객체 존재 확인에 유용합니다.

// ✅ 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) {}
    
    // ✅ weak_ptr 반환
    std::weak_ptr<Node> getWeak() {
        return weak_from_this();  // C++17
    }
    
    void process() {
        std::cout << "Processing " << value_ << '\n';
    }
};

int main() {
    std::weak_ptr<Node> weakNode;
    
    {
        auto node = std::make_shared<Node>(42);
        weakNode = node->getWeak();
        
        // weak_ptr → shared_ptr 변환 (lock)
        if (auto shared = weakNode.lock()) {
            shared->process();  // ✅ 안전
        }
    }  // node 소멸
    
    // 객체가 소멸된 후
    if (auto shared = weakNode.lock()) {
        shared->process();
    } else {
        std::cout << "Node가 이미 소멸됨\n";  // ✅ 이 경로
    }
}

출력:

Processing 42
Node가 이미 소멸됨

Observer 패턴에서 활용

// ✅ Observer 패턴 - 순환 참조 방지
#include <memory>
#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';
    }
    
    // ✅ weak_ptr 반환 (순환 참조 방지)
    std::weak_ptr<Observer> getWeak() {
        return weak_from_this();
    }
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;  // ✅ weak_ptr로 저장
    
public:
    void addObserver(std::weak_ptr<Observer> observer) {
        observers_.push_back(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 1 소멸
Observer 2 소멸

소멸 후 알림:

6. 실전 패턴: 비동기 작업

HTTP 클라이언트

// ✅ 비동기 HTTP 클라이언트
#include <memory>
#include <thread>
#include <iostream>
#include <functional>
#include <chrono>

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) {
        // shared_from_this()로 객체 수명 보장
        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 종료

타이머 이벤트 핸들러

// ✅ 타이머 이벤트 핸들러
#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() {
        stopped_ = true;
    }
    
    void start(int intervalMs, int repeatCount) {
        auto self = shared_from_this();
        
        std::thread([self, intervalMs, repeatCount]() {
            for (int i = 0; i < repeatCount && !self->stopped_; ++i) {
                std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
                self->onTimer(i + 1);
            }
        }).detach();
    }
    
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::seconds(2));
    }  // timer 소멸 - 하지만 stopped_ = true로 안전하게 종료
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "main 종료\n";
}

7. 주의사항과 함정

1. 스택 객체에서 사용 불가

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

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

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

해결책: 항상 std::make_shared로 생성.

2. 다중 상속 시 모호성

// ❌ 다중 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()가 어느 것인지 모호함
};

해결책: 한 번만 상속.

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

3. 소멸자에서 shared_from_this() 호출

// ⚠️ 소멸자에서 호출 가능하지만 조심
class Widget : public std::enable_shared_from_this<Widget> {
public:
    ~Widget() {
        // ⚠️ 소멸 중이므로 위험
        // auto self = shared_from_this();  // 가능하지만 권장 안 함
    }
};

4. 순환 참조 주의

// ❌ 순환 참조 발생
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> next;  // ❌ shared_ptr
    
    void setNext(std::shared_ptr<Node> n) {
        next = n;
        n->next = shared_from_this();  // ❌ 순환 참조!
    }
};

// ✅ weak_ptr로 해결
class Node : public std::enable_shared_from_this<Node> {
public:
    std::weak_ptr<Node> next;  // ✅ weak_ptr
    
    void setNext(std::shared_ptr<Node> n) {
        next = n;
        if (auto shared = n->next.lock()) {
            // ...
        }
    }
};

8. 실전 베스트 프랙티스

1. 팩토리 패턴 사용

// ✅ 팩토리 패턴으로 생성 강제
class AsyncTask : public std::enable_shared_from_this<AsyncTask> {
private:
    AsyncTask() = default;  // private 생성자
    
public:
    static std::shared_ptr<AsyncTask> create() {
        auto task = std::shared_ptr<AsyncTask>(new AsyncTask());
        task->initialize();
        return task;
    }
    
private:
    void initialize() {
        auto self = shared_from_this();
        // 초기화 작업
    }
};

2. weak_ptr로 순환 참조 방지

// ✅ weak_ptr 활용
class Parent : public std::enable_shared_from_this<Parent> {
public:
    std::vector<std::weak_ptr<Child>> children;  // weak_ptr
};

class Child : public std::enable_shared_from_this<Child> {
public:
    std::weak_ptr<Parent> parent;  // weak_ptr
};

3. 람다 캡처 시 명시적으로

// ✅ 명시적으로 shared_ptr 캡처
class Worker : public std::enable_shared_from_this<Worker> {
public:
    void startWork() {
        auto self = shared_from_this();  // 명시적으로 shared_ptr 생성
        
        std::thread([self]() {  // 람다로 캡처
            self->doWork();
        }).detach();
    }
};

4. RAII로 수명 관리

// ✅ RAII 패턴
class Resource : public std::enable_shared_from_this<Resource> {
public:
    class Handle {
    private:
        std::shared_ptr<Resource> resource_;
        
    public:
        explicit Handle(std::shared_ptr<Resource> res)
            : resource_(std::move(res)) {
            resource_->acquire();
        }
        
        ~Handle() {
            if (resource_) {
                resource_->release();
            }
        }
    };
    
    Handle getHandle() {
        return Handle(shared_from_this());
    }
    
private:
    void acquire() { /* 리소스 획득 */ }
    void release() { /* 리소스 해제 */ }
};

9. 정리 및 결론

핵심 정리

항목설명
용도this를 shared_ptr로 안전하게 변환
상속std::enable_shared_from_this<T>
메서드shared_from_this(), weak_from_this() (C++17)
주의생성자에서 호출 불가 (bad_weak_ptr)
필수shared_ptr로 관리되는 객체에서만 사용

사용 시나리오

// 1. 비동기 작업
class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
public:
    void startAsync() {
        auto self = shared_from_this();
        std::thread([self]() { self->work(); }).detach();
    }
};

// 2. 콜백 등록
class EventHandler : public std::enable_shared_from_this<EventHandler> {
public:
    void registerCallback() {
        auto self = shared_from_this();
        eventSystem.on("click", [self](Event e) {
            self->handleClick(e);
        });
    }
};

// 3. Observer 패턴
class Observer : public std::enable_shared_from_this<Observer> {
public:
    std::weak_ptr<Observer> getWeak() {
        return weak_from_this();  // 순환 참조 방지
    }
};

체크리스트

enable_shared_from_this를 사용하기 전에:

  • 객체가 shared_ptr로 관리되는가?
  • 생성자에서 shared_from_this() 호출하지 않는가?
  • 스택 객체가 아닌가?
  • 다중 상속으로 모호성이 없는가?
  • 순환 참조를 weak_ptr로 방지하는가?

다음 단계

더 깊이 공부하려면:

  1. 스마트 포인터 전반 (unique_ptr, shared_ptr, weak_ptr)
  2. 순환 참조 문제와 해결책
  3. 비동기 프로그래밍 패턴
  4. Observer 패턴 구현
  5. RAII 원칙 심화

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

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


이 글이 도움이 되셨나요? enable_shared_from_this를 활용한 안전한 비동기 프로그래밍에 도움이 되었기를 바랍니다!