[C++] 스마트 포인터: shared_ptr
이전 포스트: "자원 관리와 스마트 포인터"
https://m.blog.naver.com/sssang97/221805519374
shared_ptr는 이름대로 '공유'가 가능한 포인터다. 유일성 때문에 복사 및 공유가 불가능한 unique_ptr과 상반된다.
이건 복사 연산과 복사된 포인터의 소멸을 추적해 참조 횟수(reference count)를 계산한다. 참조횟수는 그 객체를 점유하고 있는 변수의 숫자다.
그리고 참조횟수가 0이 되면 할당을 해제한다.
unique_ptr에 비해 사용이 편리해, 이외의 네이티브 언어에서도 곧잘 사용되는 편이다.
Swift에서는 기본 포인터가 이거다.
일단 한번 써보자.
포인터 타입으로 사용할 간단한 정수 래퍼 타입이다.


그럼 이제 저걸로 shared_ptr을 사용해보자.
shared_ptr은 템플릿 클래스다. 인자로 포인터의 원본 타입을 받으며, 생성자 인자로는 할당된 포인터값을 받는다.
그리고 일반 포인터와 마찬가지로 *, -> 연산자를 사용할 수 있다.

잘 돌고, 알아서 잘 해제되는 걸 볼 수 있다.
main 함수가 종료시 변수 p가 소멸되어, 참조횟수 0을 찍고 해제된 것이다.
복사
그리고 앞서 언급했듯 복사도 자유롭다.
일반적인 상황에선 그냥 생각없이 사용해도 된다.


부작용: 순환참조
근데 이 참조횟수 계산 방식에는 크나큰 결함이 존재한다.
만약 2개의 shared_ptr가 서로를 참조하면, 순환참조라는 게 발생해 해제가 수행되지 않고 메모리가 누수될 수 있다는 것이다.
코드를 한번 보자. 클래스 A는 B의 포인터를 가지며, B는 A의 포인터를 가진다.


그리고 서로를 참조하도록 설정해주면

해제가 되지 않는다.
둘중 한놈이라도 참조횟수가 0이 되어야 해제가 될 텐데. 끈덕지게 계속 물고있기 때문이다.
지역변수 a가 해제돼도 A의 참조횟수는 1이다. b에서 물고있기 때문에.
지역변수 b가 해제돼도 B의 참조횟수는 1이다...
이러한 구조는 가급적 피하는 것이 좋다.
약한 참조: weak_ptr
근데 사람 일이란게 꼭 그렇게만 되겠나?
그래서 C++에서도 순환참조를 방지하기 위한 기능을 제공하는데, 그게 바로 weak_ptr(약한참조)라는 녀석이다.
weak_ptr은 shared_ptr로부터 값을 복사받을 수 있지만, 참조횟수는 계산하지 않는다.

그럼 B에서 실질적으로 A를 갖고있지 않으므로, 지역변수 a 소멸시 A는 제대로 해제된다.
A 해제시 B의 참조횟수는 1로 줄어들고, 지역변수 b 소멸시 B도 잘 해제될 것이다.

이렇게.
근데 weak_ptr은 완전한 참조가 아니라, shared_ptr을 참조하는 거라서 사용이 좀 불편하다.
weak_ptr에서 실제 객체를 가져오려면 lock 메서드로 shared_ptr를 생성해 가져와야 한다.


원본 객체가 살아있지 않을 경우엔 빈 shared_ptr 객체를 반환한다.


성능
shared_ptr의 성능은 일반 포인터보단 떨어진다.
복사, 소멸시마다 참조횟수를 증감하고 체크해야 하기 때문이다.
게다가 메모리도 추가로 잡아먹는다.
참조횟수 계산을 위한 상태변수를 내부적으로 하나 가지기 때문이다.
그래도 그냥 가비지컬렉터로 돌아가는 것보단 수백배는 빠르고 효율적이다.
좀 더 편리한 생성: make_shared
위에선 스마트포인터를 생성할때, 생성자 인자로 직접 new를 해서 보내줬었다.
근데 꼭 그럴 필요가 있는걸까?
게다가 저러면 타입 표현이 중복된다.
shared_ptr<Int> p(new Int(99));
제너릭 인자로 Int 하나, 또 new할때 Int 하나.
보기에 썩 좋지는 않다.
make_shared는 이러한 표현을 좀더 간소화해주는 생성용 함수다.
제너릭 인자로 타입을 써주고, 함수 인자에는 생성자의 인자들만 가변인자로 보내주면 된다.
그러면 알아서 내부적으로 new 생성해준다.


더 자세한 규격과 기능들에 관해서는, 문서를 참조하길 바란다.
https://en.cppreference.com/w/cpp/memory/shared_ptr