#cpp #tr1 shared_ptr, weak_ptr

1 minute read

STL에는 auto_ptr라는 스마트 포인터가 있지만 할당하면 소멸식 복사(destructive copy)로 자원에 대한 소유권을 넘겨주는 동작을 한다. STL 알고리즘은 값에 의한 복사가 기본 동작이라서 컨테이너에 못 넣는 스마트 포인터가 되겠다. 이게 이슈가 많이 돼서 이름도 있다. COAP(Container Of Auto_Ptr).

shared_ptrTR1(Technical Report 1)에서 추가된 스마트 포인터이다. 소유권을 넘겨주는 동작이 아니라 소유권을 나눠 가지는 스마트 포인터다. 우리가 흔히 스마트 포인터라 부르는 것처럼 동작한다. 자원 관리로는 아주 친근한 우리 친구 레퍼런스 카운터(reference counter)를 사용한다. 자원을 소유한 객체가 늘어나면 레퍼런스 카운터를 증가시키고 그 객체가 삭제되면 레퍼런스 카운터를 감소시켜서 결국 0이 되면 이 자원은 이제 사용하지 않으니깐 삭제한다.

이 레퍼런스 카운터가 간단하면서도 킹왕짱인것 같으나 당연히 문제점이 존재한다. 이름하여 순환 참조(cyclic reference, reference cycle). 교착 상태(deadlock)과 비슷하다. 스마트 포인터 두 객체가 서로 가리키고 있으면 프로그램이 종료돼도 레퍼런스 카운터가 0이 아니게 된다. 아까운 자원이 안드로메다로 가는 상황. 마치 두 사람이 서로 볼때기를 잡고 “니가 먼저 놓으면 놓아줄게”, “웃기고 있네. 너부터 놓으면 내가 놓으마.”라고 말하고 계속 잡는 상황과 같다. 현실에서는 하나, 둘, 셋 하고 동시에 놓겠지만, 컴퓨터에는 그런 거 없다.

레퍼런스 카운터를 직접 조작하면 순환 참조를 해결할 수 있다. 물론 직접 조작하니 위험한 방법. Loki::SmartPtr 같은 경우는 단위전략 기반의 디자인(policy-based design)이어서 자원 관리 전략을 우리가 직접 만들 수 있지만 tr1에 shared_ptr은 자원 관리 전략을 안에 숨겨 놓았다. 위험하니깐. 대신 사용할 수 있는 클래스를 만들었는데, 이게 weak_ptr이다. 만들거나 지워도 자원 레퍼런스 카운터를 전혀 만지지 않는다. 그래서 스마트 포인터 두 객체가 서로 가리키고 있어도 순환 참조가 일어나지 않게 된다. 레퍼런스 카운터를 만지지 않고 들여다보기만 하기 때문에 자원이 유효한지 weak_ptr::expired()로 확인해서 사용해야 한다.

#include <memory>
#include <iostream>
#include <string>

#define CYCLIC_REFERENCE 0

struct Node;
typedef std::tr1::shared_ptr<Node> NodeSharedPtr;
typedef std::tr1::weak_ptr<Node> NodeWeakPtr;

struct Node
{
    explicit Node(const std::string& name)
        : Name(name)
    {}

    ~Node()
    {
        std::cout << "~Node : " << Name << std::endl;
    }

    std::string     Name;
#if CYCLIC_REFERENCE
    NodeSharedPtr   Next;
#else
    NodeWeakPtr     Next;
#endif
};

int main()
{
    NodeSharedPtr test1(new Node("test1"));
    NodeSharedPtr test2(new Node("test2"));

    // 순환 참조(cyclic reference)
    test1->Next = test2;
    test2->Next = test1;

    return 0;
}

예제를 억지로 만들어 내긴 했지만 일어날 수 있는 상황이다. shared_ptr를 기본으로 사용하되 순환 참조가 일어날 수 있는 곳에 weak_ptr를 사용하면 된다.