-
[C++] shared_ptr 은 어떻게 동작하게 될까?쾌락없는 책임 (공부)/C++ 짜잘이 2022. 8. 9. 20:19반응형
서론
스마트 포인터를 공부하게 되면 unique_ptr을 먼저 본 뒤 shared_ptr, weak_ptr을 보게 됩니다. 그러면 각 스마트 포인터들이 어째서 등장하게 되었는지 쉽게 알 수 있기 때문이죠. 저 같은 경우에도 위와 같은 흐름으로 공부를 했습니다.
일단 개념적인 부분들을 전부 알아낸 뒤 아직 사용은 해보지 않아 사용 시 어떠한 문제들이 있는지는 아직 잘 모르는 상태입니다. 그런데 shared_ptr의 경우 어떤 식으로 reference count를 하는지 궁금하게 되어 알아보게 되었고, 이에 따라 정리되는 지식들을 이렇게 블로그에 쓰는 중입니다.
shared_ptr과 weak_ptr을 간단히 보면
일단 기존 unique_ptr의 경우 객체에 대한 소유권을 한 포인터에서만 가질 수 있게 관리하기에 간혹 여러군데에서 소유권을 가져야 하는 경우 unique_ptr대신 사용할 스마트 포인터가 필요하게 됩니다. 이렇게 등장한 게 shared_ptr이죠.
이름처럼 공유를 할 수 있는 스마트포인터로
std::shared_ptr<Some> sptr1(new Some()); std::shared_ptr<Some> sptr2(ptr1)
이런 식으로 기존 uniqur_ptr에서 사용할 수 없던 방식으로 스마트 포인터를 만들 수 있게 됩니다. 그런데 이런 shared_ptr의 경우 만일 서로를 참조하고 있는 상호 참조 관계가 된다면 문제가 됩니다. 그 경우 A, B 객체가 있다고 했을 때 서로를 참조하기 때문에 reference count가 1 밑으로 떨어지지 않는다는 문제가 생기게 됩니다. 이런 경우를 위해서 등장한 것이 weak_ptr 인 것이죠.
+ C#에서 상호 참조를 제거하는 방법
weak_ptr의 경우 스마트 포인터의 일종으로 당연하게 객체 관리를 해 주지만 shared_ptr에 있는 참조 카운트를 증가시키지는 않습니다. 때문에 객체의 소멸 여부는 shared_ptr 이 얼마나 가리키고 있는지가 중요한 것이죠. 이러한 특징으로 인해 weak_ptr을 만들 때는 shared_ptr이나 다른 weak_ptr을 통해서 제작해야 합니다. 그리고 이 weak_ptr을 사용하기 위해서는 shared_ptr로 변경을 해야 할 필요가 있으며 이 과정에서 내부에 정의된 lock함수를 사용하게 됩니다.
std::shared_ptr<Some> makeSharedPtr = wPtr1.lock(); if(makeSharedPtr) { // do something }
만일 shared_ptr이 아무것도 가리키지 않으면 false가 되기에 위 if를 추가해 체크할 수 있게 됩니다.
그러면 shared_ptr은 어떻게 reference counting을 할까?
그러면 여기서 궁금해지는 게 shared_ptr은 어떻게 참조 카운트를 세고 있을까요? 일단 제가 제일 먼저 생각한 것은 내부적으로 변수를 둬서 이를 계산하자인데 이는 잘 생각해보면 사용해서는 안 되는 방법이 됩니다. 이렇게 되면 이후에 등장하는 shared_ptr과 이전에 만들어둔 shared_ptr 간 참조 카운트가 맞지 않게 됩니다.
std::shared_ptr<Some> sptr1(new Some()); std::shared_ptr<Some> sptr2(p1); std::shared_ptr<Some> sptr3(p2); std::cout << sptr1.use_count(); // 2 std::cout << sptr2.use_count(); // 3 std::cout << sptr3.use_count(); // 3
위 그림처럼 3개째 shared_ptr을 만드는 순간 이전의 shared_ptr에게 추가 참조 카운트가 생겼다는 것을 알려주지 못하게 됩니다. 때문에 이런 문제를 해결하기 위해서 shared_ptr에서는 공통으로 사용하는 제어 블록(Control Bolck)을 사용하게 됩니다.
shared_ptr이 새로 생성이 되면 그 객체를 참조하는 카운트를 해결하기 위한 제어 블록을 동적 할당하게 됩니다. 이후 그 블록의 위치만 공유하면 되니 그 블록에 참조 카운트를 두고 관리하게 되는 것입니다.
객체 생성 시 주소 값을 주면 새 제어 블록이 생긴다
위 제목의 말대로 shared_ptr에 주소 값, 객체 등을 넘겨주면 제어 블록을 계속 만들게 됩니다.
Some* s = new Some(); std::shared_ptr<Some> sptr1(s); // 제어블록 생성! std::shared_ptr<Some> sptr2(s); // 제어블록 생성!
만일 이러면 sptr1의 수명이 끝나는 순간 s를 없애버리기 때문에 sptr2가 더 사용하지 못한다는 오류가 생기게 됩니다. 그러면 한 객체로 shared_ptr을 만들었는지 계속 확인해야 할까요?
일단 최대한 주소 값으로 생성하는 걸 피해야 하지만 이를 계속 피할 수는 없으니 여기서 enable_shared_from_this로 해결할 수 있게 됩니다.
class A : public std::enable_shared_from_this<A> { int *data; public: A() { data = new int[100]; std::cout << "자원을 획득함!" << std::endl; } ~A() { std::cout << "소멸자 호출!" << std::endl; delete[] data; } std::shared_ptr<A> get_shared_ptr() { return shared_from_this(); } }; 코드 출처 : https://modoocode.com/252
enable_shared_from_this 클래스를 상속받는다면 shared_from_this 함수가 있는데 이를 이용해서 제어 블록을 한 개만 사용할 수 있게 됩니다.
std::shared_ptr<A> pa1 = std::make_shared<A>(); std::shared_ptr<A> pa2 = pa1->get_shared_ptr();
이렇게 말이죠. 다만 따로 상속을 받지 않는다면... 뭐 이전처럼 제어블록을 추가로 만들게 되거나 get_shared_ptr이 동작하지 않게 되겠죠.
shared_from_this() 함수는 단순히 제어 블록 환인용 함수라고 합니다. 때문에 제어 블록을 만들지는 않기에 제어블록이 없는 객체로 shared_ptr을 생성하면 오류가 난다고 합니다.
weak_ptr과 shared_ptr의 제어 블록
위 서론에서 weak_ptr은 shared_ptr처럼 참조 카운트를 크게 가지지 않는다고 했습니다. 여기서 약한 참조 카운팅이라 하는데 이는 직접적으로 객체 소멸에 영향을 주는 변수가 아닙니다. 그런데 만약 shared_ptr의 참조 카운트가 0 이 되면 제어 블록은 어떻게 될까요?
이 경우에는 제어 블록이 사라지지 않게 됩니다. 바로 제어블록이 사라지만 참조 카운트가 없음을 알 수 없기 때문이죠. 또는 weak_ptr이 제어 블록이라고 보고 있는 곳이 이후 다른 메모리로 덮어지면 이상하게 판단할 수 있기 때문에 제어 블록은 바로 해제하지 않는다고 합니다.
참고 자료
- 사실상 제가 정리하는 글보다 여기를 보는 걸 추천드립니다. 엄청나거든요
반응형'쾌락없는 책임 (공부) > C++ 짜잘이' 카테고리의 다른 글
[C++] C++ sort는 어떤 알고리즘을 사용할까 (0) 2022.08.13 [C++] C++ iterator, 반복자 - 1 (0) 2022.08.13 [C++] C++의 move semantics (의미론적 이동?) (0) 2022.08.07 [C++] C++ 로 중복 없는 랜덤 변수 만드는 방법 - 1 (0) 2022.08.05 [C++] C++ 의 '1LL'은 무슨 뜻일까? (0) 2022.06.16