shared_ptr

 

일반적으로 하나의 자원은 한개의 스마트 포인터에 의해 소유되는것이 바람직하다.

 

하지만 여러 개의 스마트 포인터가 하나의 객체를 같이 소유해야 하는 경우가 발생할 수 있다.

여러 객체에서 하나의 자원을 사용하는 경우 나중에 해당 자원을 해제하려면 해당 자원을 사용하는 모든 객체가 소멸되어야 하는데 어떤 객체가 먼저 소멸될지, 언제 소멸될지 알 수 없으므로 특정 자원을 몇개의 객체에서 가리키는지 추적해서 0이 되면 그때 해제시켜주는 방식의 포인터가 필요하며 그게 shared_ptr

 

즉 unique_ptr과 달리 한 shared_ptr이 객체를 가리켜도 다른 shared_ptr도 그 객체를 가리킬 수 있다.

std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1);  // p2 역시 생성된 객체 A 를 가리킨다.

// 반면에 unique_ptr 의 경우
std::unique_ptr<A> p1(new A());
std::unique_ptr<A> p2(p1);  // 컴파일 오류!

 

현재 shared_ptr의 참조 개수가 몇개인지 알려면 아래처럼 use_count() 함수 사용

std::shared_ptr<A> p1(new A());
std::cout << p1.use_count(); // 여기서 p1.use_count() 결과는 1

std::shared_ptr<A> p2(p1); // p2에 p1을 복사생성자로 초기화
std::cout << p1.use_count(); // 여기서 p1.use_count() 결과는 2

std::shared_ptr<A> p3;
p3 = p1; // 대입을 이용한 초기화, 여기서 p1.use_count()는 3

마지막 shared_ptr의 수명이 다하면 참조횟수가 0이 되면서 delete해주면 됨

 

shared_ptr 또한 make 함수가 지원되며 이걸 사용하는게 훨씬 좋다

auto p1 = std::make_shared<A>(); // std::shared_ptr<A> p1 = std::make_shared<A>();

왜냐하면 std::shared_ptr<A> p1(new A()); 방식의 경우 new로 동적할당이 발생하고, shared_ptr의 제어블록을 만드는데 동적할당이 또 발생하므로 많은 리소스를 쓰는 동적할당을 두번하게 된다.

make_shared로 한번에 하는게 더 이득

 

 

 

shared_ptr 사용시 주의할 점

 

shared_ptr은 인자로 주소값이 전달되면 자신이 해당 객체를 처음 소유하는 shared_ptr로 행동한다.

그렇다보니 처음 만들어진 shared_ptr이 하는 행동인 제어블록을 만드는 행동을 하게되는데

A* a = new A();
std::shared_ptr<A> pa1(a);
std::shared_ptr<A> pa2(a);

위처럼 두번 같은 주소값으로 shared_ptr을 생성하면 제어블록이 두개가 되어버린다.

그럼 또 참조 count가 1이되므로 pa1이 소멸시 pa2가 살아있음에도 A를 소멸시켜버린다.

 

그러므로 shared_ptr을 주소값을 통해 생성하는 것은 지양해야 한다.

 

하지만 객체 내부에서 자기 자신을 가리키는 shared_ptr을 만든다면 this 포인터를 사용해야 하는데 이럴땐 주소값을 통해 생성할 수 밖에 없다.

이 경우 enable_shared_from_this를 사용한다.

클래스에 : public std::enable_shared_from_this<A> 를 상속받고 shared_from_this() 함수를 통해 본인의 포인터를 리턴한다.

조건으로는 이미 해당 객체의 shared_ptr이 먼저 정의되어 있어야 함

 

 

 

weak_ptr

 

만약 위처럼 서로를 참조하는 shared_ptr이 있다면 이 둘은 절대 해제될수가 없음

만약 트리 구조라면 부모 노드가 여러개의 자식 노드를 가지므로 shared_ptr을 사용할텐데

자식노드도 부모노드를 기억하기 위해 shared_ptr을 사용한다면 위와 같은 문제가 생김

 

위와 같은 순환 참조를 해결하기 위해 존재하는게 weak_ptr

weak_ptr은 일반 포인터와 shared_ptr 사이에 위치한 스마트 포인터로, 스마트 포인터처럼 안전하게 객체를 참조할 수 있게 해주지만 shared_ptr과 달리 참조 개수를 늘리지는 않음

 

weak_ptr이 만약 어떤 객체를 가리키고 있더라도 다른 shared_ptr이 가리키고 있지 않다면 메모리에서 소멸됨

그러므로 weak_ptr 자체로는 원래 객체를 참조할 수 없고, 반드시 shared_ptr로 변환해서 사용해야 함

이 때 가리키고 있는 객체가 이미 소멸되었다면 비어있는 shared_ptr로 변환되고, 아닐 경우 해당 객체를 가리키는 shared_ptr로 변환됨

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class A {
  std::string s;
  std::weak_ptr<A> other;

 public:
  A(const std::string& s) : s(s) { std::cout << "자원을 획득함!" << std::endl; }

  ~A() { std::cout << "소멸자 호출!" << std::endl; }

  void set_other(std::weak_ptr<A> o) { other = o; }
  void access_other() {
    std::shared_ptr<A> o = other.lock();
    if (o) {
      std::cout << "접근 : " << o->name() << std::endl;
    } else {
      std::cout << "이미 소멸됨 ㅠ" << std::endl;
    }
  }
  std::string name() { return s; }
};

int main() {
  std::vector<std::shared_ptr<A>> vec;
  vec.push_back(std::make_shared<A>("자원 1"));
  vec.push_back(std::make_shared<A>("자원 2"));

  vec[0]->set_other(vec[1]);
  vec[1]->set_other(vec[0]);

  // pa 와 pb 의 ref count 는 그대로다.
  std::cout << "vec[0] ref count : " << vec[0].use_count() << std::endl;
  std::cout << "vec[1] ref count : " << vec[1].use_count() << std::endl;

  // weak_ptr 로 해당 객체 접근하기
  vec[0]->access_other();

  // 벡터 마지막 원소 제거 (vec[1] 소멸)
  vec.pop_back();
  vec[0]->access_other();  // 접근 실패!
}

lock 함수로 weak_ptr이 가리키는 객체가 아직 메모리에 살아있다면 해당 객체를 가리키는 shared_ptr을 반환하고, 이미 해제가 되었다면 아무것도 가리키지 않는 shared_ptr을 반환함

 

제어블록에는 shared_ptr의 참조개수는 물론이고 weak_ptr의 참조 개수도 기록되며 둘 다 0이어야 해당 메모리들을 해제함

 

 

 

 

※ 참고 문헌

https://modoocode.com/252