[EMC++] Item 19. 소유권 공유 자원의 관리에는 std::shared_ptr을 사용해라
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)을 가리키는 포인터를 가지고 있기 때문이다.
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_ptr
나std::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_ptr
는 std::unique_ptr
로 변환할 수 없다.
이는 std::shared_ptr
의 reference count가 1이어도 마찬가지다.
std::shared_ptr는 배열을 관리할 수 없음
std::shared_ptr
는 std::uniuqe_ptr
와 다르게 단일 객체만 관리할 수 있게 설계되었다.
즉, std::shared_ptr<T>
는 가능하지만 std::shared_ptr<T[]>
는 불가능하다.
또한 std::shared_ptr
는 operator[]
를 제공하지 않는다.
따라서 C++11에서 배열 객체를 관리하고 싶으면 std::array
혹은 std::vector
를 쓰는 것이 바람직하다.