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로 두 번째 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되었으므로, 기본 캡처에 기대지 말고 캡처 대상을 명시하는 편이 좋습니다.