Post List

2015년 4월 15일 수요일

item 20: Dangle pointer 체크에는 std::weak_ptr을 사용하자.

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점









Effective Modern C++ - Scott Meyers
Item 19: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle.

item 19. Dangle pointer 체크에는 std::weak_ptr을 사용하자.

이제 C++14의 대표적인 스마트 포인터 중에 마지막인 std::weak_ptr에 대해서 소개하겠습니다.


std::weak_ptr의 특징

1. std::shared_ptr로 부터 생성됩니다. ( 물론 std::weak_ptr로 부터도 생성이 됩니다. )
   즉, std::weak_ptr은 std::shared_ptr이 가리키는 개체만 가리킬수 있습니다.
   하지만 std::shared_ptr의 참조 카운트 (reference count)는 증가시키지 않습니다.
   제어블록 (control block)엔 weak count가 따로 있습니다.

2. dereference 연산자 ( * )가 없습니다. (누가 봐도 단점)
   즉, std::weak_ptr을 이용해서는 개체를 직접 제어하지 못합니다.

3. 가리키는 개체가 expired() 되었는지 확인이 가능합니다. (누가 봐도 장점)
   즉, 짝으로 사용되는 std::shared_ptrdangle pointer인지 여부 확인이 가능합니다.

이 3가지가 전부 입니다.
위 특징만 봐서는 솔직히 별로 쓸모 없어 보입니다.
하지만, 몇 몇 케이스에서 꽤나 유용한 기능을 제공해 줍니다.


먼제 위 특징들에 대해서 좀 더 자세히 설명을 하고,
다음으로 유용하게 사용되는 케이스에 대해서 말씀드리겠습니다.


std::weak_ptr의 생성 방법

std::shared_ptr을 생성자의 인자로 넣어주면 됩니다. 끝 !

std::shared_ptr<Widget> SPW = std::make_shared<Widget>();

std::weak_ptr<Widget> WPW(SPW);


dangle pointer 확인 방법

확인 방법은 3가지가 있습니다.

expired() 라는 멤버 함수를 사용해서 확인이 가능합니다.

SPW = nullptr;

if (WPW.expired())
   std::cout << "SPW is expired" << std::endl;

2 std::shared_ptr생성을 위하여 lock()이라는 멤버 함수를 사용해서 reutrn 되는 값이 nullptr 인지 아닌지로 확인 가능합니다.

auto SPW1 = WPW.lock();

if (SPW1 == nullptr)
    std::cout << "SPW1 is expired" << std::endl;

lock() 함수는 std::shared_ptr 타입을 return 해주기 때문에 auto를 사용해도 괜찮습니다.

std::shared_ptr의 생성자에 std::weak_ptr을 넣어서 예외가 발생하는지 여부로 확인이 가능합니다.

try {
    std::shared_ptr<Widget> SPW2(WPW);
catch (std::bad_weak_ptr &e) {
    std::cout << "SPW2 is expired : " << e.what() <<  std::endl;
}

참고로 위 코드에서 SPW2를 auto로 선언하면std::weak_ptr이 생성됩니다.


dereference 연산자 ( * ) 가 제공되지 않는 이유

아주 간단하게 말씀드리자면, std::weak_ptr은 해당 개체의 수명에 관여하지 않기 때문입니다.
dereference 연산자를 이용하여 개체를 사용하는 동안,
다른 쓰레드에서 해당 개체를 가리키고 있는 마지막 std::shared_ptr이 해제될 경우,
비정상적인 동작 (undefined behavior)이 일어 날 수 있습니다.


std::weak_ptr이 유용하게 사용되는 예시

1 Caching Object

Factory method 에서 Widget 을 reutrn 받는 경우를 들어보겠습니다.

std::unique_ptr<const Widget> LoadWidget(unsigned int ID);

일단은 std::unique_ptr로 reutrn받도록 해 보았습니다.

특정 ID에 대한 Widget를 만들기 위해서는 D/B나 외부 네트워크 (TCP/IP)를 통해서 읽어와야 한다던지,
아니면 복잡한 계산에 의해서 생성됩니다.
그리고 특정 ID에 대해서 Widget 생성 요청이 자주 일어난다면,
Cache를 이용하여 Widget 개체를 거기에 올려놓은 다음, Cache에 해당 개체가 있는 동안은 그것을 사용하도록 하면 좋을 것입니다.

그럼 일단 여러 곳에서 공유해야 하니 std::unique_ptr은 안되겠고, std::shared_ptr로 해야겠군요.

std::shared_ptr<const Widget> LoadWidgetCache(unsigned int ID)
{
    static std::unordered_map<unsigned int, std::weak_ptr<const Widget>> cache;

    auto SP = cache.at(ID).lock(); // std::shared_ptr

    if (SP == nullptr)
    {
       SP = LoadWidget(ID);        // std::shared_ptr <- std::unique_ptr
       cache[ID] = SP;             // std::weak_ptr <- std::shared_ptr
    }
    return SP;
}

std::unordered_map을 cache로 이용하였습니다.
이미 만들어졌고, 아직 다른 std::shared_ptr에게 참조되고 있어서 살아있는 경우는 바로 해당 개체애 대한 std::shared_ptr을 return하고,
아니면 새로 만들어서 cache에 넣은 다음 return합니다.

2. Observer List

Observer Design Pattern에 대해서 혹시 모르는 분 있으신가요 ?

그렇다면 먼저 거기에 대해서 아주 간단하게 설명해 드리겠습니다.
Subject (개체) 와 Observer (감시자) 가 있습니다.
Observer 는 Subject의 상태를 계속 체크하고 있습니다.
그러다가 상태가 변하면 Observer가 그것을 Notify(통지) 해줍니다.

아래 Link에서 간단히 확인이 가능합니다.
http://blog.naver.com/icysword/220062750648
( 아직 옮겨오지 못해서 Naver Blog 입니다. )

구현 방법은 Subject가 Observer 들의 포인터를 list 형태로 가지고 있게 하고는.
자신의 상태가 변경될때 해당되는 List 안에 있는 모든 Observer의 포인터를 이용해서 해당 함수를 호출하는 형식입니다.

여기서 Subject는 Observer 들이 언제 해제 될지에 대해서는 관심이 없지만,
해당 Observer가 해제 되었는지 아닌지에 대해서는 알고 있어야 합니다.

이럴 경우에 Observer 의 std::weak_ptr을 가지고 있으면, 각 Observer를 실행하기 전에 dangle pointer 인지 검사를 할 수 있어서 편리합니다.

3. std::shared_ptr의 순환 참조 (circular reference) 방지



위 그림과 같이 A 와 C 는 B 개체를 std::shared_ptr를 통하여 공유하고 있습니다.
이때 B가 A의 포인터를 가지고 하는 경우가 있습니다.
어떤 형식으로 가지고 있는게 좋은지 한 번 생각해 보겠습니다.

3.1 Raw Pointer

A가 먼저 해제 되고, C는 아직 유요할 경우에 B는 A에 대한 dangle pointer를 가지고 있게 됩니다.
문제가 일어날 수 있겠군요.

3.2 std::shared_ptr

A가 먼저 해제를 시도 합니다.
그런데 B가 A의 std::shared_ptr을 가지고 있어서 참조카운트가 0이 아니라 1 이므로 해제되지 않습니다.
A가 해제되지 않았으므로 B의 참조카운트도 2 그대로 입니다.
하지만 A는 이미 { ... } 의 범위를 벗어나서 더이상 사용은 불가능한 상태입니다.
C가 해제되면 B의 참조카운트 값이 1이 됩니다.
A 와 C가 더이상 소스코드 상에서 사용하는 곳이 없더라도,
A, B의 참조카운트 값은 1로 유지되면서 해제되지 않습니다.
결과적으로 A,B 개체 및 그 control block에 대해서 memory leak이 발생하게 됩니다.

3.3 std::weak_ptr

결론부터 말하자면 최고의 선택입니다.
A가 해제될 경우 B는 A에 대하여 dangle pointer 감지가 가능합니다.
B가 A를 가리키는 포인터는 참조카운트에 영향을 미치지 않으므로,
아무 문제없이 잘 동작합니다.


하지만, 굳이 저렇게까지 쓸 일은 잘 없습니다.
엄격한 자료 구조 ( strictly data structure ) 중 하나인 Tree에 대해서 생각해 보겠습니다.
Tree 같은 자료 구조 (data structure) 를 예로 들어보자면,
부모 노드 가 자식 노드를 가리키는 포인터를 가지고 있습니다.
부모 노드는 여러 개의 자식 노드에 대한 포인터를 가지고 있지만,
자식 노드에게 부모 노드는 하나밖에 없습니다.
이럴 경우 부모 노드는 자식 노드에 대해서 std::unique_ptr을 가지고 있으면 되고,
자식 노드는 부모 노드의 Raw Pointer를 가지고 있어도 문제 될 일이 없습니다.
자식 입장에서 부모 노드가 dangle pointer가 될 일은 없기 때문입니다.

std::weak_ptr 효율성

효율성의 측면에서 보자면 std::weak_ptr와 std::shared_ptr는 거의 똑같다고 봐도 됩니다.

1. 크기가 같습니다.
2. 같은 제어블록 (control block)을 사용합니다.
3. 생성(constructor), 해제(destructor), 대입(assignment)은 참조카운트 (weak count : std::shared_ptr와는 다름) 를 atomic 연산해 줘야 합니다.

한가지 다른 점이 있다면,
std::shared_ptr의 참조카운터가 0이 되면, 관리하고 있는 개체를 해제시키고,
그 상태에서 std::weak_ptr의 참조카운터도 0이 되면 제어블록을 해제시킵니다.

* Things to remember

std::weak_ptr은 dangle pointer 감지가 가능합니다.

- Caching, Observer List, std::shared_ptr의 순환참조 (circular reference) 등은 std::weak_ptr를 사용하면 좋은 대표적인 사례입니다.

댓글 없음:

댓글 쓰기