반응형

 이번장에서는 weak_ptr에 대해서 알아 보도록 합니다. 

 shared_ptr를 구현하면서 참조 카운트에 영향을 받지 않는 스마트 포인터가 필요했는데

 weak_ptr을 사용하면 shared_ptr가 관리하는 자원(메모리)을 참조카운트에 영향을 미치지 않으면서 참조 타입으로

 가질 수 있습니다. 

weak_ptr 특징

 weak_ptr은 직접적으로 자원을 할당 받을 수 없습니다. 

 weak_ptr이 자원에 대한 참조를 받으려면 shared_ptr이나 다른 weak_ptr의 자원을 복사 생성자나 대입 연산자를 통해서

 할당 받을 수 있습니다. 

 weak_ptr은 자원을 할당 받아도 참조 카운팅에 영향을 미치지 않습니다.

 (내부적으로 _Weaks 라는 변수를 가지고 있지만 객체의 생성 및 해제에는 영향을 미치지 않습니다.)

 할당 받은 자원을 사용하려면 lock이라는 멤버 함수를 통해서 사용할 수 있습니다. 

 (lock이 성공하면 shared_ptr 객체를 리턴해서 사용 할 수 있습니다.)

 만약 할당 받은 자원의 shared_ptr의 참조 카운트가 0이 되어 객체가 해제 된다면 expire되서 더 이상 weak_ptr을

 사용할 수 없습니다. 만약 expire되었을 때 lock 함수를 호출하면 empty상태의 shared_ptr을 리턴합니다.

 shared_ptr과 마찬가지로 empty상태의 weak_ptr은 자원 및 control block의 주소를 가지지 않습니다. 

 이후 weak_ptr의 원활한 설명을 위해서 Person 클래스를 사용하도록 하겠습니다.

// weak_ptr을 설명하기 위한 예제 circular reference(순환 참조) class
class Person
{
    std::string m_name;
    std::shared_ptr<Person> m_partner; // initially created empty

public:

    Person(const std::string& name) : m_name(name)
    {
        std::cout << m_name << " created\n";
    }
    ~Person()
    {
        std::cout << m_name << " destroyed\n";
    }

    bool SetParter(std::shared_ptr<Person>& p)
    {
        if (!p)
            return false;

        this->m_partner = p;

        std::cout << m_name << " is now partnered with " << p->m_name << "\n";

        return true;
    }

};

weak_ptr 객체 생성하기

    // shared_ptr로 pWoong 객체 생성
    auto pWoong = std::make_shared<Person>("woong"); 

    // shared_ptr을 weak_ptr의 복사생성자로 weak_ptr(weakp1) 객체 생성
    std::weak_ptr<Person> weakp1(pWoong);

    // weak_ptr을 weak_ptr의 대입연사자로 weak_ptr(weakp2) 객체 생성
    std::weak_ptr<Person> weakp2 = weakp1;

weak_ptr로 shared_ptr 객체 참조시 메모리 상태

 shared_ptr의 객체인 pWoong을 생성 한 후에 복사생성자를 통해서 weak_ptr 객체인 weakp1을 생성 하였고

 weakp1을 대입해서 weakp2을 생성하는 로직을 실행한 후에 메모리 상태를 확인 해보았습니다. 

 shared_ptr의 객체 생성과는 다르게 _Uses(참조 카운트)가 변하지 않은 것을 확인 할 수 있습니다. 

 대신에 _Weaks의 객체가 3으로 변경된 것을 확인 할 수 있습니다.

 _Weaks의 기본값은 1이고 2개의 weak_ptr이 할당되서 3이 되었습니다. 

 이 상태에서는 pWoong의 객체만 해제 되더라도 자원의 할당을 해제합니다.

 weakp1과 weakp2의 객체는 자원에 영향을 미치지 못합니다.

weak_ptr의 할당 받은 자원 사용 하기

 weak_ptr은 할당 받은 자원을 직접적으로 사용하지 못합니다.

 직접적인 사용을 막기 위해서 operator-> 및 operator*와 get() 함수가 구현되어 있지 않습니다.

 왜 이렇게 구현했을까요? 그 이유는 weak_ptr 객체에 접근할 수 있다고 해서 weak_ptr이 소유한 자원에 접근 할 수

 있다는 보장이 없기 때문에 이렇게 설계 했습니다. 그럼 weak_ptr의 객체를 어떻게 사용할까요?

 lock함수를 통해서 우회적으로 사용을 지원합니다. 

_NODISCARD shared_ptr<_Ty> lock() const noexcept { // convert to shared_ptr
    // shared_ptr 객체를 생성하고
    shared_ptr<_Ty> _Ret;
    // weak_ptr의 객체를 통해서 shared_ptr을 생성합니다. 
    (void) _Ret._Construct_from_weak(*this);
    return _Ret;
}

 lock 멤버함수의 원형입니다. weak_ptr의 lock함수를 호출하면 내부적으로 shared_ptr의 객체를 만들고 리턴합니다. 

template <class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other) noexcept {
    // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
    // weak_ptr의 control block이 살아 있는지 확인하고
    // 참조카운트를 증가시킵니다. (_Incref_nz 함수)
    if (_Other._Rep && _Other._Rep->_Incref_nz()) {
        _Ptr = _Other._Ptr;
        _Rep = _Other._Rep;
        return true;
    }

    return false;
}

 _Construct_from_weak 함수의 구현을 확인해보면 weak_ptr의 control block를 존재하는지 검사 한 후

 lock을 통해서 리턴하는 shared_ptr을 위해서 참조 카운트를 증가 시킵니다.

 내부적으로 이렇게 설계되어 있기 때문에 lock으로 리턴 받은 shared_ptr을 사용 도중에 객체 자원이

 참조카운트가 0이 되는 것을 방지하여 객체가 무효화하는 것을 방지합니다.

 (안전성을 지키기 위해  많은 고민이 된 설계라고 할 수 있습니다.)

lock함수를 사용했을때 객체들의 상태

// shared_ptr로 pWoong 객체 생성
auto pWoong = std::make_shared<Person>("woong"); 
auto pSoo = std::make_shared<Person>("soo");

// shared_ptr을 weak_ptr의 복사생성자로 weak_ptr(weakp1) 객체 생성
std::weak_ptr<Person> weakp1(pWoong);
// weak_ptr은 자원을 직접적으로 접근할 수 없습니다.
weakp1->SetPartner(pSoo) // error 발생!!

{
    // lock함수를 통해서 shared_ptr로 변환 후 사용
    std::shared_ptr<Person> sharedp1 = weakp1.lock();
    if(0 != sharedp1.use_count())
        sharedp1->SetPartner(pSoo);
}

 예제처럼 lock함수를 통해서 새로운 shread_ptr을 할당 받아서 자원에 대해서 접근할 수 있습니다. 

멀티 스레드 환경에서 weak_ptr 사용

 weak_ptr을 생성할 때와 lock함수를 사용해서 shared_ptr을 할 때 _Weaks와 _Useds(참조카운트)가 변경되는 것을

 확인 할 수 있습니다.

 shared_ptr 설명 때 _Useds가 Interlocked 함수로 멀티스레드에서 안전하게 동작하게 구현되어 있다고 

 설명 드렸습니다. _Weaks 또한 Interlocked 함수로 구현되어 있습니다. 

 weak_ptr이 생성될 때는 _Weaks를 통한 스레드 동기화 비용이 발생하며 lock 함수를 사용할 때는

 shared_ptr을 생성하기 위해서 _Useds의 수정으로 인한 스레드 동기화 비용이 발생 합니다.

 이러한 내부 구현을 이해해서 코드를 구현을 설계하면 더욱 효율적으로 동작할 수 있습니다. 

weak_ptr을 사용하는 이유

 shared_ptr을 우회적으로 사용하도록 구성되어 있다면 직접적으로 shared_ptr을 사용하지 않고

 왜 weak_ptr을 사용해야 할까요?

 그 이유는 shared_ptr만으로는 완전한 스마트 포인터의 기능을 제공하지 못하기 때문입니다. 

 대표적으로 circular reference(순환 참조) 문제가 있습니다. 

class Person
{
    //... 생략
    std::shared_ptr<Person> m_partner; 
    //... 생략
public:
    bool SetParter(std::shared_ptr<Person>& p)
    {
        if (!p)
            return false;

        this->m_partner = p;

        std::cout << m_name << " is now partnered with " << p->m_name << "\n";

        return true;
    }
    //... 생략
}
{
    // woong과 soo라는 Person의 shared_ptr 객체를 생성합니다.
    auto pWoong = std::make_shared<Person>("woong");
    auto pSoo = std::make_shared<Person>("soo");

    // woong과 soo과 서로 파트너로 설정합니다. 
    // 내부적으로 서로가 서로를 참조하는 형태로 구현됩니다.
    pWoong->SetPartner(pSoo);
    pSoo->SetPartner(pWoong);
}

순환참조로 객체가 해제 않됨
순환참조로 구성된 shared_ptr 구조
순환참조 구조에서 객체가 해제 되어도 삭제되지 않는 이유

위의 코드를 작성해서 수행해보면 스코프가 끝나도 shared_ptr이 내부 객체가 해제되지 않는 것을 확인 할 수 있습니다. 

SetPartner멤버함수를 통해서 pWoong은 pSoo를 내부 변수로 소유하고 pSoo는 pWoong을 내부 변수로 소유합니다.

 서로가 참조를 하고 있기 때문에 참조 카운트가 0으로 감소하지 않습니다. 이러한 상태를 순환 참조라고 합니다. 

class Person
{
    //... 생략
    std::weak_ptr<Person> m_partner; 
    //... 생략
public:
    //... 생략
}
{
    // woong과 soo라는 Person의 shared_ptr 객체를 생성합니다.
    auto pWoong = std::make_shared<Person>("woong");
    auto pSoo = std::make_shared<Person>("soo");

    // woong과 soo과 서로 파트너로 설정합니다. 
    // 내부적으로 서로가 서로를 참조하는 형태로 구현됩니다.
    pWoong->SetPartner(pSoo);
    pSoo->SetPartner(pWoong);
}

weak_ptr로 변경된후 결과
weak_ptr를 통한 순환참조 개선

 이렇게 순환 참조를 방지하기 위해서 weak_ptr을 사용합니다.

 m_partner를 weak_ptr로 설계하면 참조 카운트에 영향을 미치지 않기 때문에 순환 참조의 문제가 해결 됩니다. 

 이렇게 shared_ptr의 참조 카운트에 영향을 미치지 않아야 하는 상황이 나온다면 weak_ptr을 고려해야 합니다.

 하지만 위에서도 설명 드렸듯이 꼭 필요한 경우가 아니라면 shared_ptr로 구현해야합니다.

 (weak_ptr 사용시 lock()함수 호출로 인한 비용 증가)

자주 사용하는 멤버 함수

expired 함수

// @설명 : 할당 받은 객체가 사용가능한 상태인지 확인합니다.
_NODISCARD bool expired() const noexcept {
    return this->use_count() == 0;
}

 할당받은 shared_ptr 객체의 use_count가 0이면 true, 아니면 false를 리턴합니다.

 해당 객체가 사용 만료되었는지 체크합니다. 

reset 함수

void reset() noexcept { // release resource, convert to null weak_ptr object
    weak_ptr().swap(*this);
}

소유한 자원을 Release 합니다. 

swap함수

// 입력된 weak_ptr 변경합니다. 
void swap(weak_ptr& _Other) noexcept {
    this->_Swap(_Other);
}

 입력된 weak_ptr과 자신을 바꿉니다. 

use_count 함수

 shared_ptr과 동일합니다. 할당받은 자원의 참조 카운트 수를 리턴 받습니다.

이동연산자

 weak_ptr도 이동 연산자를 지원합니다.

반응형

'c++ > modern c++' 카테고리의 다른 글

[c++] std::move와 std::forward  (4) 2020.03.07
[c++] Move semantics  (0) 2020.03.07
[c++] 람다(Lambda) 표현식  (0) 2020.03.04
[c++] shared_ptr  (0) 2020.02.29
[c++] unique_ptr  (0) 2020.02.27

+ Recent posts