[Effective Modern C++] 19. 소유권 공유 자원의 관리에는 std::shared_ptr를 사용하라
shared_ptr의 특징
shared_ptr은 unique_ptr과 다르게 여러 객체에서 한 객체를 소유할 수 있다.
shared_ptr은 객체의 소멸 시점을 관리하기 위하여 reference count를 사용하는데, 이 count가 0이 되면 메모리를 해제한다.
shared_ptr의 내부 구현은 다음과 같다.
shared_ptr의 크기는 raw포인터의 두배이다.
raw포인터의 크기와 reference count를 가리키는 포인터를 가지고 있어야 하기 때문이다.
참조 횟수를 담을 메모리를 반드시 동적으로 할당해야 한다.
reference count를 가리키는 객체는 공유되는 자원이기 때문에 동적으로 할당해야 한다.
참조 횟수의 증가와 감소가 반드시 원자적(atomic) 연산이어야 한다.
멀티스레드 프로그램에서 shared_ptr을 사용할 수 있기 때문에 atomic 하게 증감을 해야 한다. 따라서 atomic연산으로 인해 성능 감소가 있고, 이 연산을 줄이면 성능을 높일 수 있어 그중 한 가지 방법으로 이동 연산을 사용한다.
shared_ptr의 파괴
기본적으로 delete를 통해 파괴되나 커스텀 삭제자도 구현할 수 있다.
shared_ptr의 커스텀삭제자는 unique_ptr의 커스텀 삭제자와 다르게 삭제자의 타입이 스마트 포인터 타입의 일부가 아니다.
아래의 코드를 보자.
auto loggingDel = [](Widget *pw) // 커스텀 삭제자
{
makeLogEntry(pw);
delete pw;
}
// unique_ptr: 삭제자의 타입이 포인터 타입의 일부
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
// shared_ptr: 삭제자의 타입이 포인터 타입의 일부가 아님
std::shared_ptr<Widget> spw(new Widget, loggingDel);
따라서 shared_ptr의 설계가 더 유연하며, 사용하는 커스텀 삭제자의 타입이 서로 다른 두 p1, p2는 같은 타입이므로 그 타입의 객체들을 담는 컨테이너 안에 집어넣을 수 있다.
또한, 하나를 다른 하나에 대입할 수도 있고 둘 다 shared_ptr<Widget>타입의 매개변수를 받는 함수에도 넘겨줄 수 있다.
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 };
그리고 커스텀 삭제자가 아무리 커져도 shared_ptr의 크기는 변하지 않는다. 커스텀 삭제자는 shared_ptr객체가 아니라 제어 블록에 담기기 때문이다.
shared_ptr가 관리하는 객체당 하나의 제어블록이 존재하고 커스텀 삭제자를 지정했다면, 참조 카운트와 함께 그 커스텀 삭제자의 복사본이 제어블록에 담긴다. 만약 커스텀 할당자를 지정했다면 그 할당자의 복사본도 제어블록에 담긴다. 그 외에도 제어블록에는 약한 카운트라고 부르는 이차적인 참조 카운트가 포함되며 그 밖의 추가 데이터가 포함될 수 있으나 일단 추가 데이터의 존재를 무시한다.
제어블록(Control Block)
제어블록은 shared_ptr의 소멸에 대해 관리하는 객체이다. 앞서 말한 reference count와 커스텀 삭제자를 가지고 관리하는 객체가 바로 이 제어블록이다.
제어블록 객체가 이미 생성되어 있다면 다시 생성해서는 안되며 존재여부를 알 수는 없지만 몇 가지 규칙들을 유추할 수 있다.
std::make_shared는 항상 제어블록을 생성한다.
이 함수는 항상 새로운 shared_ptr객체를 생성하기 때문에 그 객체에 대한 제어블록이 이미 존재할 가능성이 없다.
고유 소유권 포인터(std::unique_ptr이나 std::auto_ptr)로부터 std::shared_ptr 객체를 생성하면 제어블록이 생성된다.
고유 소유권 포인터는 제어블록을 사용하지 않기 때문에 그 객체에 대한 제어블록이 존재할 가능성은 없다.
raw포인터로 std::shared_ptr 생성자를 호출하면 제어블록이 생성된다.
raw포인터의 경우에는 제어블록이 없을 것이라 가정하여 제어블록을 생성한다. 만약 이미 제어블록이 있다면 생성자로 shared_ptr이나 weak_ptr을 사용하면 전달된 스마트 포인터들이 이미 필요한 제어블록을 가리키고 있기 때문에 제어블록을 만들지 않는다.
이 규칙에서 비롯되는 한가지 결과는 하나의 raw포인터로 여러 개의 shared_ptr을 생성하면 피지칭 객체에 여러 개의 제어블록이 만들어지므로 미정의 행동이 된다는 점이다.
제어블록이 여러개라는 것은 참조 카운트가 여러 개라는 뜻이며, 참조 카운트가 여러 개라는 것은 해당 객체가 여러 번 파괴된다는 뜻이다.
shared_ptr은 unique_ptr에 비해 더 많은 비용이 들지만, 그 비용보다 동적 할당 자원의 수명이 자동으로 관리된다는 이득이 생긴다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
2022.09.18 -
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
2022.09.18 -
[Effective Modern C++] 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라
[Effective Modern C++] 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라
2022.09.18 -
[Effective Modern C++] 17. 특수 멤버 함수들의 자동 작성 조건을 숙지하자
[Effective Modern C++] 17. 특수 멤버 함수들의 자동 작성 조건을 숙지하자
2022.09.11