shared_ptr은 참조 횟수(reference count)에 의해 소유중인 자원의 파괴 여부가 결정된다.

shared_ptr의 생성자는 참조 횟수를 증가시키고 소멸자는 참조 횟수를 감소시킨다. 이 때 감소된 참조 횟수가 0이 되면 더 이상 해당 자원을 소유중인 객체(스마트포인터)가 없다는 의미이기 때문에 자원을 파괴시킨다.

 

이 참조횟수는 같은 shared_ptr끼리 공유되어야 하기 때문에 공유자원에 해당되는 원시 포인터, 참조횟수를 위한 원시 포인터 총 2개를 사용하여 원시 포인터 크기의 2배의 공간을 사용하게 된다.

또한 참조 횟수를 담을 공간은 반드시 동적으로 할당해야 하고 참조 횟수의 증감이 반드시 원자적 연산이어야 한다.

 

shared_ptr이 생성되면 참조 횟수가 증가되지만 이동 연산에 의해 shared_ptr이 옮겨진 경우에는 참조 횟수가 변하지 않는다.

 

unique_ptr처럼 shared_ptr역시 자원 파괴시 기본적으로 delete를 사용한다. 물론 커스텀 삭제자도 지원하지만 unique_ptr과는 약간 차이가 있다.

 

auto loggingDel = [](Widget* pw) {
    makeLogEntry(pw);
    delete pw;
};

std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
std::shared_ptr<Widget> spw(new Widget, loggingDel);

shared_ptr은 커스텀 삭제자의 지정에 템플릿 매개변수를 사용하지 않기 때문에 설계가 더 유연하다.

 

auto customDeleter1 = [](Widget* pw) {...};
auto customDeleter2 = [](Widget* pw) {...};

std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

std::vector<shared_ptr<Widget>> vpw { pw1, pw2 }; // ok

shared_ptr의 경우 커스텀 삭제자와 관계없이 공유자원의 타입만 같으면 되기 때문에 같은 컨테이너로 관리할 수 있다. 하지만 unique_ptr은 커스텀 삭제자가 다르면 타입도 다르기 때문에 같은 컨테이너로 관리할 수 없다.

 

또한 커스텀 삭제자의 상태 갯수와 무관하게 shared_ptr 객체의 크기는 변하지 않는다. 그렇다고 커스텀 삭제자의 크기가 줄어드는 것은 아니고, 커스텀 삭제자가 사용하는 메모리는 shared_ptr 객체의 일부가 아닐 뿐이다.

 

shared_ptr은 참조 횟수, 약참조 횟수, 커스텀 삭제자 등등을 관리하는 제어 블록(위에서 언급한 참조횟수 관리를 위한 원시 포인터)을 가지게 되는데 이 제어 블록이 새로 생성되는 조건은 다음과 같다.

 

◾ make_shared에 의해 shared_ptr이 생성될 때

◾ unique_ptr로부터 shared_ptr를 생성할 때

◾ 원시 포인터로 shared_ptr 생성자를 호출할 때

 

이미 제어 블록이 있는 객체로부터 shared_ptr를 생성하고 싶다면 원시 포인터가 아니라 shared_ptr 또는 weak_ptr를 생성자의 인수로 지정하면 된다.

만약 동일한 원시 포인터로 다수의 shared_ptr의 생성자를 호출하여 객체를 생성하게 되면 서로 다른 자원으로 인식하여 참조 횟수가 다르게 관리된다. 이 상태에서 어느 한쪽의 shared_ptr의 소멸자가 호출되어 자원을 파괴하게 되면 다른 shared_ptr은 파괴되어 존재하지 않는 자원을 소유하고 있는것이기 때문에 이후에 사용하거나 소멸자가 호출될 때 문제가 발생한다.

 

같은 포인터를 소유하고 있지만 참조횟수가 다르게 관리된다

참조횟수를 공유하려면 sp2의 생성자에 원시 포인터가 아니라 sp를 넣어주면 된다.

 

이런 문제를 가지고있기 때문에 shared_ptr의 생성자에 원시 포인터를 그대로 넣어주는것은 피해야 한다.

가급적 make_shared를 사용해야 하지만 make_shared는 커스텀 삭제자를 지정할 수 없기 때문에 커스텀 삭제자를 지정하거나 생성자에 원시 포인터를 넣을 수 밖에 없는 상황이라면 new의 결과를 직접 전달하는 것이 좋다.

 

만약 shared_ptr의 생성자에 this 포인터가 사용되면 위에서 언급한 문제가 발생할 가능성이 높다. 외부에서 해당 객체를 가리키는 다른 shared_ptr이 존재할 수도 있기 때문이다.

 

재미있게도 이런 상황을 위한 템플릿이 존재한다.

 

class Widget : public std::enable_shared_from_this<Widget> {
public:
    ...
    void process();
};

이름도 재밌다. 묘하게 되풀이되는 템플릿 패턴(Curiously Recurring Template Pattern, CRTP)이다. 이것도 일종의 디자인 패턴이다. 자기 자신을 템플릿 인자로 사용해서 클래스 템플릿 인스턴스화로부터 파생되게 하는 pimpl 관용구이다.

 

enable_shared_from_this는 현재 객체를 가리키는 shared_ptr를 생성하되, 제어 블록을 복제하지는 않는 멤버 함수 shared_from_this를 정의한다. 제어 블록을 복제하지 않기 때문에 클래스 멤버 함수에서 this 포인터로 shared_ptr를 만들어야 하는 경우에는 shared_from_this를 호출하면 된다.

 

void Widget::process() {
    processWidgets.emplace_back(shared_from_this());
}

 

 

자세한 내용은 넘어가도록 한다.

 

여러모로 복잡한 점들이 있지만 shared_ptr의 연산은 비싸지 않고, 설령 추가비용이 발생한다 하더라도 동적 할당된 자원의 수명이 자동으로 관리된다는 점은 매우 크나큰 이점이다.

만약 shared_ptr에 의한 추가 비용이 걱정된다면 추가 비용보다는 shared_ptr이 반드시 필요한지부터 고민해봐야 한다.

경우에 따라서는 unique_ptr이 더 나은 선택일수도 있기 때문이다.

 

덧붙여서 unique_ptr로부터 shared_ptr를 생성할 수 있지만 반대는 성립하지 않는다.

또한 shared_ptr은 단일 객체를 가리키는 포인터를 염두에 두고 설계된 것이기 때문에 배열을 사용하는것은 그다지 좋은 선택이 아니다. 애초에 array, vector 등이 제공되기 때문에 사용될 이유도 없다.

 

◾ std::shared_ptr는 임의의 공유 자원의 수명을 편리하게(가비지 컬렉션에 맡길때만큼이나) 관리할 수 있는 수단을 제공한다.

◾ 대체로 std::shared_ptr 객체는 그 크기가 std::unique_ptr 객체의 두 배이며, 제어 블록에 관련한 추가 부담을 유발하며, 원자적 참조 횟수 조작을 요구한다.

◾ 자원은 기본적으로 delete를 통해 파괴되나, 커스텀 삭제자도 지원된다. 삭제자의 타입은 std::shared_ptr의 타입에 아무런 영향도 미치지 않는다.

◾ 원시 포인터 타입의 변수로부터 std::shared_ptr를 생성하는 일은 피해야 한다.

+ Recent posts