[EMC++] Item 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용해라
Item 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용해라
std::weak_ptr
std::weak_ptr
는 std::shared_ptr
와 비슷하지만 reference count에 영향을 주지 않는 스마트 포인터이다.
reference count 대신 객체 수명에 영향을 주지 않는 weak count를 사용한다.
즉, 객체를 참조하되 객체의 수명에는 영향을 주고 싶지 않을 때 사용한다.
std::weak_ptr
는 std::weak_ptr::expired
멤버 함수를 통해 자신이 가리키고 있는 객체가 파괴되었는지 확인할 수 있다.
// reference count는 1
std::shared_ptr<Widget> spw = std::make_shared<Widget>();
// 여전히 reference count는 1
std::weak_ptr<Widget> wpw(spw);
// reference count는 0으로 Widget 파괴, spw는 dangling pointer
spw = nullptr;
// wpw가 가리키는 객체가 파괴되었는지 확인 가능
if(wpw.expired())
std::weak_ptr
는 그 자체로는 역참조(*
)할 수도 없고 nullptr
인지 확인할 수도 없는 불완전한 포인터이다.
따라서 std::weak_ptr
가 가리키는 객체에 접근하기 위해서는 std::shared_ptr
로 변환해서 사용해야 한다.
이때 std::weak_ptr
가 가리키는 객체가 만료인지 확인하고 std::shared_ptr
로 변환해야 하는데 이 과정이 atomic 해야 한다. (아니면 멀티스레드 환경에서 문제가 발생할 수 있기 때문이다.)
해당 방법으로는 std::weak_ptr::lock
멤버 함수를 사용해 std::shared_ptr
객체를 반환하는 방법과 std::weak_ptr
를 인자로 받는 std::shared_ptr
생성자를 호출하는 방법이 있다.
/* std::weak_ptr::lock 멤버 함수를 사용하는 경우 */
std::shared_ptr<Widget> spw1 = wpw.lock(); // wpw가 만료이면 spw1는 nullptr
auto spw2 = wpw.lock(); // wpw가 만료이면 spw2는 nullptr (auto로 더 깔끔하게 표현 가능)
/* std::weak_ptr를 인자로 받는 std::shared_ptr 생성자를 호출하는 경우 */
std::shared_ptr<Widget> spw3(wpw); // wpw가 만료이면 std::bad_weak_ptr 에러 발생
예시에서 알 수 있듯이 두 방법은 std::weak_ptr
가 가리키는 객체가 만료일 때 처리하는 방식이 다르다.
std::weak_ptr의 용도
std::weak_ptr
가 유용하게 사용되는 경우는 다음과 같다.
caching에 사용하는 경우
특정 객체를 생성하고 해당 객체를 가리키는 포인터를 반환하는 팩토리 함수가 있다고 가정하자.
Item 18에서는 std::unique_ptr
를 반환 타입으로 사용하여 해당 객체에 대한 소유권도 함께 넘겨 주었다.
// 해당 id에 맞는 객체 포인터를 반환하는 팩토리 함수
::unique_ptr<const Widget> loadWidget(WidgetId id);
하지만 loadWidget
함수의 비용이 큰 상황에서는 특정 id에 대한 객체 포인터 정보를 caching 할 필요가 있다.
std::unique_ptr
는 객체에 대한 소유권을 넘겨줘야 하므로 caching이 불가능하지만, 다음과 같이 std::shared_ptr
와 std::weak_ptr
를 사용하면 객체의 수명에 영향을 끼치지 않으면서 객체 포인터를 caching 하는 함수를 구현할 수 있다.
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) {
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
// 객체가 cache에 없으면 nullptr
auto objPtr = cache[id].lock();
if (!objPtr) {
// cache에 없으면 추가
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr;
}
observer pattern에 사용하는 경우
observer pattern은 subject(관찰 대상, 상태가 변할 수 있는 객체)와 observer(관찰자, 상태 변화를 통지 받는 객체)로 구성된 디자인 패턴이다.
대부분의 observer pattern 구현에서 subject 객체들은 자신을 관찰하는 observer 객체 포인터들을 멤버 변수로 저장하고 있다. (상태 변화가 생기면 통지해주기 편리하기 때문에)
이때 subject 객체는 파괴된 observer 객체에 접근하는 일이 없도록 std::weak_ptr
를 사용하여 observer 객체 포인터들을 저장할 수 있다.
std::shared_ptr의 circular reference 해결에 사용하는 경우
다음과 같이 A, B, C 객체가 있고 A, C가 std::shared_ptr
를 통해 B에 대한 소유권을 공유하고 있다고 가정해보자.
이때 B에서 A를 가리키는 포인터가 추가로 필요하다면 어떤 종류의 포인터를 사용해야 할까?
다음과 같은 3가지 선택지가 존재한다.
- raw pointer
std::shared_ptr
std::weak_ptr
raw pointer를 사용할 경우, A가 먼저 파괴되면 B가 가지고 있는 A 포인터는 dangling pointer가 되지만 B는 이를 알지 못하고 역참조해서 문제가 발생할 수 있다.
std::shared_ptr
를 사용할 경우, A, B가 서로를 참조하는 circular reference가 발생하여 A, B 자원 모두 삭제가 불가능하다.
std::weak_ptr
를 사용할 경우, 앞서 두 선택지에서 발생하는 문제 없이 사용이 가능하다.
즉, std::weak_ptr
를 사용하면 dangling pointer 여부를 확인할 수 있으며 circular reference 문제도 해결할 수 있다.
만약 부모-자식처럼 서로를 참조할 일이 없는 계층적(hierarchal) 구조라면
std::unique_ptr
를 사용하는 것이 바람직하다.