728x90
반응형

weak_ptr

weak_ptr는 언제든 소멸될 수 있는 객체를 사용할 때 쓰는 포인터이다.

대체로 weak_ptr은 shared_ptr을 이용해서 생성하지만 reference count를 증가시키지 않는다. 즉, 객체의 소멸에 관여하지 않는다.

대상을 잃은 weak_ptr은 만료되며 만료 여부는 멤버 함수 expired가 돌려주는 값으로 판단할 수 있다.

만료되지 않은 weak_ptr이라고 해도 역참조 연산이 없기 때문에 피지칭 객체에 직접 접근하는 것은 불가능하다.

만일 역참조 연산이 가능하게 하도록 한다고 하면, 사용하려고 하는 순간 이미 객체가 소멸되어 미정의 행동이 나올 수도 있다.

// std::weak_ptr가 가리키는 피지칭 객체를 역참조 하려면 std::weak_ptr로부터 std::shared_ptr를 얻고 그 std::shared_ptr을 역참조한다.

// 방법 1: lock 멤버 함수를 사용한다. 
std::shared_ptr<Widget> spw1 = wpw.lock(); // wpw가 만료이면 spw1은 널 
auto spw2 = wpw.lock(); // 위와 동일하나 auto를 사용했음

// 방법 2: std::shared_ptr의 생성자에 std::weak_ptr를 넘겨준다.
std::shared_ptr<Widget> spw3(wpw); // wpw가 만료이면 std::bad_weak_ptr(예외)가 발생

효율성 면에서 weak_ptr과 shared_ptr은 동일하며, 크기도 같고 같은 제어블록을 사용한다.

차이점은 weak_ptr은 객체의 소유권 공유에 참여하지 않으며, 따라서 피지칭 객체의 참조 카운트에 영향을 미치지 않고 두 번째 참조 카운트만 조작한다.

 

weak_ptr 용도

캐싱

데이터 베이스에서 가져온 데이터를 캐싱해놓는다고 가정하면, 성능상의 이슈로 인해 사용하지 않는 객체는 소멸시켜야함을 알 수 있다.

만약 캐싱한 객체에서 reference count를 증가시킨다면 소멸시점에 대해 알기 어려울 것이므로 이 경우 weak_ptr이 적절하다.

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
    static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
                              
    auto objPtr = cache[id].lock();
    
    if (!objPtr)
    {
        objPtr = loadWidget(id);
        cache[id] = objPtr;
    }
    return objPtr;
}

 

옵저버 패턴

옵저버 패턴에서 관찰자는 관찰 대상들을 담는 컨테이너를 사용한다. 관찰자들은 관찰 대상의 소멸 여부에는 관심이 있지만 수명에는 관여하지 않는다. 따라서 이 경우도 weak_ptr이 적절하다.

 

순환참조

A와 B, C 객체가 존재한다고 하고 A와 C가 B객체를 소유하는 상황을 가정해보자.

이 상황 속에서 B가 A를 가리키는 포인터가 필요하게 된 경우 어떤 포인터를 사용해야 할까?

1. raw 포인터

2. shared_ptr

3. weak_ptr

만약 raw포인터를 사용한다면 A가 소멸됐을 때 B가 여부를 알 수 없어 null ptr에 접근하게 된다.

그리고 shared_ptr은 A가 B를 소유하고 B가 A를 소유하게 되는 상황으로 순환참조를 하게 되어 서로 영원히 소멸되지 않는 문제가 생긴다.

weak_ptr은 널포인터에 접근하지도 않고 순환참조가 발생하지도 않아 가장 좋은 선택이 될 수 있다.

728x90
반응형