스토리텔링 개발자

[Effective Modern C++] 20. std::weak_ptr 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 20. std::weak_ptr

김디트 2025. 3. 5. 11:58
728x90

항목 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라

 

 

 

std::weak_ptr
  • 소유권 공유에는 참여하지 않는 스마트 포인터.
  • 즉, 관리 대상을 잃은 상황에 대한 대응이 필요하다.
  • std::shared_ptr을 보강하는 스마트 포인터이므로 API만으로는 그다지 스마트해 보이지 않는다.
auto spw = std::make_shared<Widget>(); // 레퍼런스 카운트 1
...
std::weak_ptr<Widget> wpw(spw); // 레퍼런스 카운트 1(변화 없음)
...
spw = nullptr;  // 레퍼런스 카운트 0, wpw는 관리 대상을 잃었다.

if(wpw.expired()) ... // 관리 대상을 잃었다면
  • weak_ptr은 역참조 연산이 존재하지 않는다.(operator->)
    • 그 대신 expired 체크 후 shared_ptr을 생성하여 리턴을 아토믹 연산으로 제공해준다.
    • 그것이 lock 함수이다.
std::shared_ptr<Widget> spw1 = wpw.lock(); // wpw가 만료되었으면 spw1는 null
  • 혹은 weak_ptr을 받는 shared_ptr의 생성자를 사용할 수도 있다.
std::shared_ptr<Widget> spw(wpw); // wpw가 만료라면 std::bad_weak_ptr 예외 발생

 

 

 

std::weak_ptr가 유용한 예제1
std::unique_ptr<const Widget> loadWidget(WidgetID id); // unique_ptr을 리턴하는 팩토리 함수
  • 가정 1. loadWidget의 비용이 크다.
  • 가정 2. ID들이 되풀이해서 쓰이는 경우가 많다.
  • 그렇다면 리턴값들을 캐싱하여 재활용할 수 있을 것이다.
    • 재활용하려면 unique_ptr은 좋은 선택이 아니다.
    • 리턴 받은 측에서 사용하고 난 후에 자동 파괴될 것이기 때문이다.
  • std::shared_ptr로 변경하여 캐싱하도록 재구성.
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
    // weak_ptr를 캐싱
    static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
    
    auto objPtr = cache[id].lock();
    
    if(!objPtr) // 캐시에 없거나 만료되었다면 적재
    {
        objPtr = loadWidget(id);
        cache[i] = opbjPtr;
    }
    
    return objPtr;
}

 

 

 

std::weak_ptr가 유용한 예제2
  • 관찰자(Observer) 설계 패턴
  • 관찰자 패턴에서는 관찰 대상 객체 포인터들을 관리해야 한다.
  • 허나, 관찰자 자체가 해당 대상 객체들의 레퍼런스 카운트를 올릴 필요는 없다.
    • weak_ptr를 사용하여 해결한다.

 

 

 

std::weak_ptr가 유용한 예제3
  • A와 C가 B의 소유권을 공유하고 있다.

  • B에서 A를 가리키는 포인터가 필요하게 되었다고 하면, 그 포인터는 무슨 종류여야 할까?

  • raw 포인터
    • C -> B 상황에서 A가 파괴되면, B가 가진 A에 대한 포인터는 대상을 잃게 된다.
    • 허나 B는 그 사실을 알지 못하므로 댕글링 포인터에 접근할 위험이 있다.
  • std::shared_ptr
    • A와 B는 서로를 가리킨다.
    • 순환 참조가 되어 결과적으로 A, B 둘다 절대 파괴되지 못한다.
    • 사실상 메모리 누수다.
  • std::weak_ptr
    • 위의 두 문제가 모두 해결되는 방법이다.

 

 

 

추가적인 내용
  • weak_ptr은 shared_ptr과 효율성은 동일하다.
    • 객체 크기가 동일하다.
    • 제어 블록을 사용한다.
    • 생성, 파괴, 할당 연산에 아토믹 레퍼런스 카운트 조작이 관여한다.
      • 어라? 레퍼런스 카운트에는 관여하지 않는댔지 않나?
      • 사실은 소유권 공유에 참여하지 않으므로, 관리 객체 레퍼런스 카운트에만 영향을 미치지 않을 뿐이다.
      • 제어 블록에는 다른 두 번째 레퍼런스 카운트가 있고 거기에는 관여한다.(항목 21 참조)
728x90
Comments