RAII and Memory Management(2) - boost::weak_ptr

예전에 boost::shared_ptr에 대한 글을 작성한 적이 있다. 이때, shared_ptr 내부 변수 중 weak_count에 대한 설명을 작성하려고 구글링했을 때 weak_count에 대한 내용은 없고 죄다 weak_ptr에 대한 이야기를 하고 있어 당황한 적이 있다. 아마 weak_countweak_ptr과 관련이 있지 않을까 하는 생각이 들어 shared_ptr다음 바로 weak_ptr을 주제로 포스팅하고자 한다

Weak?

weak_ptr도 스마트 포인터의 한 종류이긴 한데 이름에서부터 시름시름 앓는 소리가 들려오는 것 같다. 맞다. weak_ptr은 이를 의도한 스마트 포인터의 한 종류이다.
이를 설명하기 위해선shared_ptr의 reference count에 대한 설명이 필요하다.

Strong Count (Use count)

shared_ptr에서는 간단하게 설명했지만 shared_ptr 내부에는 2개의 count 변수가 있다. 하나는 use count라고도 불리는 Strong count, 다른 하나는 weak count이다.
Strong count의 경우 shared_ptr에서 참조하고 있는 객체가 몇 개의 다른 shared_ptr에서 참조되고 있는지를 나타내는 count이다.
코드를 통해 알아보자

class DataInfo {
public:
    DataInfo() {
        size = 0;
        permission = 0;
    }

    DataInfo(const std::string &sName, size_t sSize, int sPermission) {
        name = sName;
        size = sSize;
        permission = sPermission;
    }

    ~DataInfo() {
        std::cout << "Destroy " << name << std::endl;
    }

    std::string name;
    size_t size;
    int permission;
};

boost::shared_ptr<DataInfo> ptr = boost::make_shared<DataInfo>("Struct", 1, 2);
std::cout << "ptr ref count(1) : " << ptr.use_count() << std::endl;
{
    // 참조 카운터 1 증가
    boost::shared_ptr<DataInfo> ptr2 = ptr;
    std::cout << "ptr ref count(2) : " << ptr.use_count() << std::endl;
    std::cout << "ptr2 ref count(2) : " << ptr2.use_count() << std::endl;
}
// 블럭을 빠져나왔으므로 ptr2는 소멸됨
std::cout << "ptr ref count(3) : " << ptr.use_count() << std::endl;
ptr ref count(1) : 1
ptr ref count(2) : 2
ptr2 ref count(2) : 2
ptr ref count(3) : 1
Destroy Struct

객체를 참조하는 shared_ptr이 증가될 때마다 strong count는 증가하며 마지막에는 객체를 참조하는 shared_ptr이 모두 삭제되었으므로 본 객체의 소멸자가 호출되는 것을 볼 수 있다.

하지만 strong count만 사용하는 것은 shared_ptr가 순환 참조 (deadlock 같은 형태)되는 순간 서로의 소멸을 기다리면서 프로그램이 종료될 때까지 소멸하지 않는 문제가 발생한다. 다음의 코드를 보면 이해할 수 있다.

class A {
  int *data;
  std::shared_ptr<A> other;

 public:
  A() {
    data = new int[100];
    std::cout << "자원을 획득함!" << std::endl;
  }

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

  void set_other(std::shared_ptr<A> o) { other = o; }
};

int main() {
  std::shared_ptr<A> pa = std::make_shared<A>();
  std::shared_ptr<A> pb = std::make_shared<A>();

  pa->set_other(pb);
  pb->set_other(pa);
}

// 출처 : https://modoocode.com/252

pa와 pb를 선언하고 make_shared를 사용해 서로 다른 A 객체를 참조시켜 주었다.

선언 시점의 pa와 pb 내부 other은 참조하는 객체가 없기 때문에 ref count가 0이고, pa와 pb는 각각 새로운 A 객체를 할당받았으므로 ref count가 1이다.

image-20210925232607312

이후 pa의 other에 pb를 할당해주고, pb의 other에 pa를 할당하는 작업을 진행하게 되는데, 이럴 경우 다음과 같은 그림이 된다.

image-20210925232632513

기존 pa의 ref count 1에다가 pb의 other까지 참조하고 있는 그림이 되어 pa의 ref count는 2가 된다. pb도 마찬가지.
이런 상황에서 소멸 시점이 다가오면 pa 자기 자신은 소멸되나, pb의 other가 아직 소멸되지 않았기 때문에 ref count는 1로 유지되어 pa가 최초에 참조하고 있던 A 객체는 소멸되지 않는다. pb도 마찬가지.
이를 순환 참조로 인한 문제라고 하며 내부에 자기 자신을 가리키는 변수가 필요한 자료구조에서는 shared_ptr를 사용할 수 없다.

weak count

weak countshared_ptr가 객체를 참조하는 횟수가 늘어나도 증가하지 않고 weak_ptr이 참조하는 횟수에 따라 증가하고 소멸한다.
또한 이름에서 유추할 수 있듯이, weak count의 값이 몇인지 관계없이 strong count의 값이 0이 되는 순간 참조하는 객체는 사라지게 된다.

사용법

어떻게 생성해야 하는지?

weak_ptr은 shared_ptr을 이용해야만 생성될 수 있으며 일반 객체를 사용하여 생성할 수 없다.

DataInfo info("inside", 123456, 123);
// error: no matching function for call to ‘boost::weak_ptr<DataInfo>::weak_ptr(DataInfo&)’
boost::weak_ptr<DataInfo> weakPtrErr1(info);
// error: no matching function for call to ‘boost::weak_ptr<DataInfo>::weak_ptr(DataInfo*)’
boost::weak_ptr<DataInfo> weakPtrErr2(&info);

// 아래는 모두 정상 빌드
boost::shared_ptr<DataInfo> sharedPtr = boost::make_shared<DataInfo>("Struct", 1, 2);
boost::weak_ptr<DataInfo> weakPtr(sharedPtr);
boost::weak_ptr<DataInfo> weakPtr2(sharedPtr);

어떻게 사용하는지?

weak_ptr은 shared_ptr처럼 포인터 참조 연산자로 접근할 수 없고, lock() 함수를 사용해 반환되는 shared_ptr 객체로 접근해야 한다
이때 당연하겠지만 strong count는 1 증가한다

boost::shared_ptr<DataInfo2> sPtr = boost::make_shared<DataInfo2>("Struct", 1, 2);
std::cout << sPtr->name << std::endl;
boost::weak_ptr<DataInfo2> weakPtr1 = sPtr;

boost::shared_ptr<DataInfo2> sPtr2 = weakPtr1.lock();
std::cout << sPtr2->name << std::endl;

strong count와의 연관

strong count가 0이 되면 weak count의 수와 관계없이 참조하는 객체는 삭제된다.

boost::weak_ptr<DataInfo> weakPtr1;
boost::weak_ptr<DataInfo> weakPtr2;
{
    boost::shared_ptr<DataInfo> sPtr = boost::make_shared<DataInfo>("Struct", 1, 2);
    weakPtr1 = sPtr;
    weakPtr2 = sPtr;
}
std::cout << "weakPtr1 lock after block" << std::endl;
boost::shared_ptr<DataInfo> sPtr2 = weakPtr1.lock();

if (sPtr2) {
    std::cout << sPtr2->name << std::endl;
} else {
    std::cout << "sPtr2 NULL" << std::endl;
}
Destroy Struct
weakPtr1 lock after block
sPtr2 NULL

sPtr2를 선언할 때 weakPtr1이 참조하던 sPtr은 블록 탈출 시 소멸되어 strong count는 0이 되고 참조하던 객체는 소멸된다
이때 weak_ptr만 남아있는 상황이라면 lock()을 통해 가리키고 있는 객체에 접근할 경우 빈 shared_ptr 객체가 반환된다.
그러므로 weak_ptr의 lock을 사용해 shared_ptr 객체를 획득한 경우 반드시 NULL check가 필요하다

'Develop > Boost' 카테고리의 다른 글

RAII - boost::shared_ptr  (0) 2021.09.25
Data structure - Boost::any  (0) 2021.09.14