Item 19. 소유권 공유 자원의 관리에는 std::shared_ptr을 사용해라


std::shared_ptr

std::shared_ptr는 특정 객체에 대해 공유된 소유권(shared ownership)을 가지는 포인터 객체이다.

즉, std::shared_ptr는 객체를 소유하지 않고 공유하다가 아무도 객체를 사용하지 않은 시점에 파괴한다.

std::shared_ptr는 공유 자원을 관리하기 위해 참조 횟수(reference count)를 사용한다.

reference count는 현재 객체를 가리키고 있는 std::shared_ptr의 갯수를 나타낸다.

std::shared_ptr의 생성자는 reference count를 증가시키고 std::shared_ptr의 소멸자는 reference count를 감소시킨다.

reference count가 0이 되면 해당 객체를 가리키는 포인터가 없다고 판단하여 자원을 파괴한다.


shared_ptr의 성능

std::shared_ptr의 크기는 raw pointer의 2배이다. (자원을 가리키는 포인터 + 참조 횟수를 저장하는 포인터)

또한 std::shared_ptr에서 reference count를 증감하는 연산은 멀티 스레드 환경에서도 문제없이 동작해야 하므로 원자적(atomic) 연산이어야 한다.

원자적 연산은 비원자적 연산보다 느릴 수밖에 없다. 하지만 상황에 따라 이동 연산을 통해 reference count를 증가시키지 않고 사용하면 속도를 더 높일 수 있다.


std::shared_ptr의 custom deleter

custom deleter를 사용하여 std::unique_ptr를 정의할 경우 custom deleter도 포인터 타입의 일부로 포함되지만 std::shared_ptr는 타입에 포함되지 않는다.

auto loggingDel = [](Widget *pw) {
                    makeLogEntry(pw);
                    delete pw;
                  };
 
// custom deleter의 타입이 포인터 타입의 일부임
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);

// custom deleter의 타입이 포인터 타입의 일부가 아님
std::shared_ptr<Widget> spw(new Widget, loggingDel);

따라서 다른 custom deleter를 사용하는 std::shared_ptr들을 하나의 컨테이너에 담을 수 있다.

이는 std::unique_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<std::shared_ptr<Widget>> vpw{ pw1, pw2 }

또한 std::shared_ptr는 custom deleter를 사용해도 포인터 객체의 크기가 증가하지 않는다.

그 이유는 메모리가 다른 공간에 할당되기 때문인데 이를 이해하기 위해서는 std::shared_ptr의 제어 블록(control block)에 대해 알아야 한다.


std::shared_ptr의 제어 블록(control block)

std::shared_ptr의 크기는 raw pointer의 2배라고 했는데 그 이유는 자원을 가리키는 포인터와 제어 블록(control block)을 가리키는 포인터를 가지고 있기 때문이다.

image

control block은 앞서 언급한 custom deleter를 포함해 reference count, weak count 등을 저장하고 있는 자료구조이다.

std::shared_ptr가 관리하는 객체당 하나의 control block이 존재한다.

control block의 생성 규칙은 다음과 같다.

  • 고유 소유권 포인터(std::unique_ptr)로부터 std::shared_ptr를 정의하면 control block이 생성된다. (std::unique_ptr는 control block을 사용하지 않기 때문에 그 객체에 대한 control block이 이미 존재할 가능성이 없다.)
  • std::make_shared는 항상 control block이 생성된다. (std::make_shared 함수는 std::shared_ptr가 가리키는 객체를 새로 생성하기 때문에 그 객체에 대한 control block이 이미 존재할 가능성이 없다.)
  • std::shared_ptrstd::weak_ptr로부터 std::shared_ptr을 정의하면 기존 포인터 객체가 가지고 있는 control block을 참고한다.
  • raw pointer로 std::shared_ptr를 정의하면 control block이 생성된다.


std::shared_ptr 사용 시 주의할 점

std::shared_ptr 사용 시 주의할 점은 다음과 같다.

raw pointer로 std::shared_ptr의 정의을 피해야 함

raw pointer로 std::shared_ptr를 정의하는 방법은 상당히 위험하다.

앞서 control block의 생성 규칙에서 말했듯이 raw pointer로 std::shared_ptr를 정의하면 control block이 생성된다.

그럼 하나의 raw pointer로 두 개의 std::shared_ptr를 정의하면 어떻게 될까?

auto pw = new Widget;

// *pw에 대한 제어 블록이 생성됨
std::shared_ptr<Widget> spw1(pw, loggingDel); 

// *pw에 대한 두 번째 제어 블록이 생성됨
std::shared_ptr<Widget> spw2(pw, loggingDel);

이처럼 같은 객체를 가리키는 std::shared_ptr임에도 불구하고 control block이 2개가 생성되는 불상사가 발생하게 된다.

같은 객체를 가리키는 control block이 2개가 생성되면 control block의 reference count가 0이 되면서 객체를 파괴하는 상황이 2번이나 발생하여 중복 해제(double free) 문제가 생긴다.

이를 막기 위해서 std::make_shared를 사용할 수 있지만 std::make_shared는 custom deleter를 지정할 수 없다는 단점이 있다.

또다른 방법으로는 std::shared_ptr의 인자로 new의 결과를 직접 전달하는 방법이 있다.

std::shared_ptr<Widget> spw1(new Widget, loggingDel);

std::shared_ptr<Widget> spw2(spw1); // spw2는 spw1과 동일한 control block을 사용

raw pointer로 std::shared_ptr를 정의하는 문제는 this 포인터가 관여하면 더욱 복잡해진다.

std::shared_ptr들을 이용해서 Widget 객체들을 관리한다고 가정해보자.

std::vector<std::shared_ptr<Widget>> processedWidgets;

// 외부에서 Width을 가리키는 std::shared_ptr이 있다고 가정
class Widget {
public:
  void process(void);
};

void Widget::process(void) {
  processedWidgets.emplace_back(this); // 새로운 control block이 생성
}

this 포인터도 raw pointer로 볼 수 있기 때문에 processedWidgets.emplace_back(this); 구문에서 새로운 control block이 생성된다.

만약 외부에 해당 Widget을 가리키는 다른 std::shared_ptr가 이미 존재하면 중복 해제 문제가 발생할 수 있다.

이를 해결하기 위해서는 std::enable_shared_from_this 템플릿을 상속받는 방법이 존재한다.

std::shared_ptr는 std::unique_ptr로 변환할 수 없음

std::shared_ptrstd::unique_ptr로 변환할 수 없다.

이는 std::shared_ptr의 reference count가 1이어도 마찬가지다.

std::shared_ptr는 배열을 관리할 수 없음

std::shared_ptrstd::uniuqe_ptr와 다르게 단일 객체만 관리할 수 있게 설계되었다.

즉, std::shared_ptr<T>는 가능하지만 std::shared_ptr<T[]>는 불가능하다.

또한 std::shared_ptroperator[]를 제공하지 않는다.

따라서 C++11에서 배열 객체를 관리하고 싶으면 std::array 혹은 std::vector를 쓰는 것이 바람직하다.


카테고리:

업데이트: