-
Shared_ptr 내부 구조 & 주의점뜯고 또 뜯어보는 컴퓨터/씨쁠쁠 C++ 2022. 10. 26. 17:43반응형
0. shared_ptr
<이전 글: https://guru.tistory.com/112>
shared_ptr 이란, 어떤 객체에 대하여, 더 이상 사용하는 곳이 없을 때, 자동적으로 메모리를 해제해주어, 사용자의 편의성을 증가시켜 주는 녀석입니다. msdn에서는 다음과 같이 설명하고 있습니다. <동적으로 할당된 개체 주위에 참조 횟수가 계산되는 스마트 포인터를 래핑 합니다.>
shared_ptr은 내부적으로 각 객체가 몇 번 참조되었는지를 나타내는 control block을 가지게 되고, control block 이 0이 되었을 시, 객체의 메모리를 해제하게 됩니다. 원리는 이쯤 하면 되었고, shared_ptr에 대해 더 깊이 이해해보겠습니다.
1. shared_ptr의 내부 count 동작
shared_ptr 은 내부에 다음과 같은 control block을 가지고 있는데,
- T 객체에 대한 포인터
- 참조된 횟수를 세어주는 Reference Counter
- Weak Counter
- 소멸자 (Shared_ptr의 경우, unique_ptr처럼 소멸자를 지정할 수 있습니다.)
로 이뤄진 것을 확인할 수 있습니다. 이때 Ref Counter의 경우,
위와 같이 Thread Safe 하게 참조 카운트를 관리하는 것을 확인할 수 있으며, 참조 카운트가 0 이 될 시에 객체를 삭제하게 됩니다. 그런데 자세히 보게 되면, control block의 소멸자를 호출하는 것은 _Decwref( )라는 함수인데, 이는 Weak Pointer 가 가리키는 참조 횟수가 0 일 때, 호출되어 소멸하게 됩니다. (Ref Count = 0 이 되었을 시에, 가리키는 객체는 사라지지만, Control Block은 Weak Counter 가 0 이 될 때까지 남아있습니다.)
참조 순환과 같은 문제를 해결하기 위해서, Shared_ptr을 Weak_ptr로 사용할 때가 있습니다.(weak_ptr 은 참조 카운트를 늘리지 않습니다.) 그리고 만약 shared_ptr이 가리키는 참조 카운트는 0 이 되어, 객체는 소멸되었지만 weak_ptr 은 생존해 있다고 했을 때, Control_block 마저 소멸시킨다면 객체의 Count을 알 수 있는 방법이 없기에, weak_ptr이 0 이 될 때까지 control_block 소멸자를 호출하지 않습니다. (생각해보면 정말로 체계적으로 잘 짜임에 감탄합니다..)
shared_ptr 은 위 설명한 것과 같이 움직이게 되고, 이제 shared_ptr을 사용할 시 주의할 점에 대해 알아보겠습니다.
2. shared_ptr 사용 시 주의점
1. raw_pointer로 shared_ptr 생성 시 조심하라.
Raw Pointer로 shared_ptr 객체를 생성 시, 하나의 객체에 대해 여러 Control Block을 가지게 됩니다. 따라서 참조 카운트가 0 이 되었을 시에 소멸자가 2번 이상 호출된다던가, 의도되지 않는 동작들이 나오기 때문에, Raw Pointer를 직접 shared_ptr로 넘겨주어 생성할 때에는 조심해서 사용해야 합니다. (effective c++에서는 make_shared를 권장.)
예를 들어, 다음과 같은 코드는 소멸자가 여러 번 호출되어 의도치 않은 에러가 발생되게 됩니다.
{ auto pw = new Sqaure; // 객체 pw 에 대한 shared_ptr 생성 -> Ref count = 1 std::shared_ptr<Sqaure> Sq1(pw); // 같은 객체 pw 에 대한 shared_ptr 생성 -> Ref count = 1 std::shared_ptr<Square> Sq2(pw); } // 스코프를 빠져 나오면서, Sq1 -> Ref count = 0 이 되고, // Sq2 -> Ref count 또한 0 이된다. 따라서, pw 의 소멸자 2번 호출.
2. 1에 이어서, 멤버 함수로 shared_ptr을 반환할 시, 주의하라
class의 멤버 함수에서, this 포인터를 사용하여 class 자체의 포인터를 넘겨주는 것은 흔한 일입니다. 하지만 클래스 T에 대한 shared_ptr을 선언해 놓고, this를 반납하는 멤버 함수가 있게 된다면, 앞서 언급한 1번과 같은 상황이 반복되게 됩니다. 항상 같은 객체에 대해서는 같은 Control Block을 유지해야 하는데, 다른 Control Block을 유지하여 Ref Count의 불일치로 의도치 않은 상황이 벌어진다는 것입니다. (this는 Raw Pointer를 가리키기 때문에, 이러한 일이 일어나는 것은 당연한 일이다.)
이럴 때는 std::enable_shared_from_this <T>를 상속받아서, this 포인터를 반납하는 대신, shared_from_this( )를 통해 shared_ptr을 반납하도록 합니다. 예시는 다음과 같습니다.
class A: public std::enable_shared_from_this<A> { // 생략 public: std::shared_ptr<A> process() { // this 대신 shared_from_this() 를 반납 return shared_from_this(); } }
3. shared_ptr을 넘겨줄 때, 포인터 및 레퍼런스에 주의하라.
다음과 같은 코드가 존재한다고 했을 때,
// 매개변수를 통해서 shared_ptr를 복사합니다. copy-> ref count 는 증가. void Copy(std::shared_ptr<A> copy) { copy->use_count(); // +1 } // 매개변수를 통해서 shared_ptr를 참조합니다. copy-> ref count 유지. void Reference(std::shared_ptr<A>& ref) { ref->use_count(); // same }
위는 복사를 통해, Ref Count를 증가시키지만, 밑의 경우 참조를 통하여 shared_ptr 객체를 가져오게 되므로, 참조 카운트가 늘지 않습니다. 이때, Copy( )의 경우, 복사를 통한 비용이 발생한다는 점만 유의하시고 계시면 됩니다.
반응형'뜯고 또 뜯어보는 컴퓨터 > 씨쁠쁠 C++' 카테고리의 다른 글
Vtable - 어디에 존재하는가.. (0) 2022.11.03 Overloading 오버로딩 - 연산자 (0) 2022.11.02 가상 소멸자 / 가상 소멸자 사용 이유 (0) 2022.10.26 [C++] String to Int, Float, Double 자료형 / stoi, stol, stoll (0) 2022.10.26 Map 을 Vector 로 변환하기 (0) 2022.10.20