C, C++/C++ 언어 / / 2024. 7. 28.

[C++] weak_ptr에 대한 설명과 사용법

weak_ptr

weak_ptr은 shared_ptr를 지원할 목적으로 C++11에 도입된 클래스입니다.

 

따라서, 이 글을 이해하기 위해선 shared_ptr의 사전 지식이 필수입니다.

shared_ptr에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++] shared_ptr에 대한 설명과 사용법

shared_ptr 이란shared_ptr은 할당된 메모리를 자동으로 관리할 목적으로 C++ 11에 도입된 유틸리티 클래스입니다. 이 클래스를 사용하기 위해선 다음의 헤더 파일을 포함해야 합니다.#include  다

codingembers.co.kr

 

shared_ptr은 편리한 스마트 포인터( smart pointer )입니다.

그렇지만, 순환 참조( circular reference )의 문제를 자체적으로 해결하지 못하는 구조적인 약점이 있습니다.

그래서, 외부에서 도움을 받기 위해 만들어진 클래스가 weak_ptr입니다.

 

shared_ptr를 통한 상호 참조

상호 참조는 객체 a에서 다른 객체 b를 참조하고, 또 b 객체에서 a 객체를 참조하는 것을 말합니다.

이를 코드로 작성하면 아마 다음과 같을 것입니다.

struct A{
    A* m_pOther = nullptr;
};

int main(){
    A a, b;
    a.m_pOther = &b;	// 상호 참조
    b.m_pOther = &a;
}

 

이러한 관계가 shared_ptr을 사용할 때도 만들어질 수 있습니다.

class NamedObj{
    string m_name;
    shared_ptr<NamedObj> m_Link;    // 다른 NameObj객체를 가리키는 포인터

public:

    NamedObj( const string& str) : m_name(str){
        cout << m_name << " object created\n";
    }

    ~NamedObj(){
        cout << m_name << " object destroyed\n";
    }

    void LinkObj( shared_ptr<NamedObj>& obj){   // 다른 객체와 연결
        m_Link = obj;
    }
};

이 NamedObj는 이름을 가질 수 있고, 다른 객체와 연결될 수 있는 클래스입니다.

이 클래스의 두 객체 "Apple"과 "Banaba"를 다음과 같이 연결할 수 있습니다.

int main(){

    shared_ptr<NamedObj> p1 = make_shared<NamedObj>("Apple");
    shared_ptr<NamedObj> p2 = make_shared<NamedObj>("Banana");

    p1->LinkObj( p2);	// 상호 참조
    p2->LinkObj( p1);
}

이렇게 되면, 무슨 일이 생길까요?

▼출력

Apple object created
Banana object created

두 객체 모두 제대로 생성은 되었는데, 생성된 객체가 파괴되지 않았습니다.

메모리가 누수( leak )된 것이죠.

 

객체가 파괴되지 않은 이유를 찾기 위해, 하나씩 뜯어보면 원인을 알게 될 것입니다.

 

먼저, shared_ptr p1("Apple")과 p2("Banana")가 생성됩니다.

 

 

그리고, p1과 p2가 연결되는 과정에서,

p1->m_Link가 "Banana" 객체에 대해 소유권을 얻고,

p2->m_Link가 "Apple" 객체에 대해 소유권을 얻습니다.

 

상호 참조

 

이를 확인하기 위해서, 다음 코드를 실행하면 "Apple"과 "Banana"의 참조수를 볼 수 있습니다.

int main(){

    shared_ptr<NamedObj> p1 = make_shared<NamedObj>("Apple");
    shared_ptr<NamedObj> p2 = make_shared<NamedObj>("Banana");

    p1->LinkObj( p2);
    p2->LinkObj( p1);

    cout << "use_cound for Apple is " << p1.use_count() << endl;
    cout << "use_cound for Banana is " << p2.use_count() << endl;
}

▼출력

Apple object created
Banana object created
use_cound for Apple is 2
use_cound for Banana is 2

위에서 보다시피, 각 객체 "Apple"과 "Banana"에 대한 참조수가 2가 되었습니다.

이렇기 때문에, main 함수 종료 시, p1이 삭제되더라도 "Apple"에 대한 참조수가 2에서 1로 바뀔 뿐입니다.

따라서, "Apple" 객체는 삭제되지 않습니다.

마찬가지로, "Banana" 객체도 영원히 삭제되지 않습니다.

 

이러한 문제는 똑같은 이유로, "A", "B", "C"의 순환 참조( circular reference )에서도  발생합니다.

심지어, "A" 객체에서 "A" 객체를 참조하는 과정에서도 발생합니다.

int main(){

    shared_ptr<NamedObj> p1 = make_shared<NamedObj>("A");
    p1->LinkObj( p1);	// 자기 자신을 참조

    cout << "use_cound for A is " << p1.use_count() << endl;
}

▼출력

A object created
use_cound for A is 2

 

weak_ptr을 통한 해결책

위와 같은 문제가 발생하는 원인은 shared_ptr의 관리를 받는 객체에서, 다시 shared_ptr을 사용함으로써 참조수를 증가시키기 때문입니다.

 

그래서, shared_ptr 대신에 문제를 해결하기 위해 만들어진 weak_ptr을 사용합니다.

weak_ptr은 할당한 메모리 객체를 관리하는 shared_ptr을 받아서 참조할 뿐, shared_ptr이 관리하는 객체에 대한 참조수를 증가시키지 않습니다.

따라서, 증가된 참조수 때문에 삭제되지 않았던 객체는 이제 제대로 삭제될 것입니다.

 

weak_ptr을 사용하여 변경된 코드는 다음과 같습니다.

class NamedObj{
    string m_name;
    weak_ptr<NamedObj> m_Link;    // shared_ptr에서 weak_ptr로 교체

public:

    NamedObj( const string& str) : m_name(str){
        cout << m_name << " object created\n";
    }

    ~NamedObj(){
        cout << m_name << " object destroyed\n";
    }

    void LinkObj( shared_ptr<NamedObj>& obj){   // 다른 객체와 연결
        m_Link = obj;
    }
};

int main(){

    shared_ptr<NamedObj> p1 = make_shared<NamedObj>("Apple");
    shared_ptr<NamedObj> p2 = make_shared<NamedObj>("Banana");

    p1->LinkObj( p2);
    p2->LinkObj( p1);

    cout << "use_cound for Apple is " << p1.use_count() << endl;
    cout << "use_cound for Banana is " << p2.use_count() << endl;
}

▼출력

Apple object created
Banana object created
use_cound for Apple is 1
use_cound for Banana is 1
Banana object destroyed
Apple object destroyed

이제, 제대로 돌아가네요.

하지만, 아직 왜 weak_ptr를 써야 하는가에 대한 의문이 남아있습니다.

 

weak_ptr의 기능들

이전 항목에서, 순환 참조 문제를 해결하기 위해서 weak_ptr 대신 그냥 NamedObj 객체에 대한 포인터를 사용할 수도 있을 것입니다.

할당된 객체에 대한 참조수가 적절하게 관리되기만 하면 문제가 해결되니까요.

class NamedObj{
    string m_name;
    NamedObj* m_Link;    // 연결된 객체에 대한 포인터

public:
	// 일부 코드 생략...
    
    void LinkObj( shared_ptr<NamedObj>& obj){   // 다른 객체와 연결
        m_Link = obj.get();	// 포인터로 연결
    }
};

하지만, 이렇게 되면 m_Link 포인터가 가리키는 객체가 언제 삭제되는지를 알 수가 없습니다.

그리고, 만약 m_Link를 통해 삭제된 객체를 사용한다면 그 결과가 정의되지 않습니다.

 

반면, weak_ptr를 사용하면 expired 멤버 함수를 통해서 shared_prt에서 관리되고 있는 객체가 삭제되었는지 여부를 알 수 있습니다.

bool expired() const noexcept;

이 함수는 shared_ptr에 의해 관리되고 있는 객체가 만료( 삭제 )되었다면 true를 반환합니다.

 

그리고, 멤버 함수 use_count는 현재 할당한 객체를 관리하고 있는 shared_ptr들의 개수를 반환하고,

lock 멤버 함수는 할당된 객체를 관리하고 있는 shared_ptr 객체를 반환합니다.

shared_ptr<T> lock() const noexcept;

이 함수를 통해서, 실제 할당된 객체에 접근할 수 있습니다.

 

다음 예제는 위에 설명한 weak_ptr 멤버 함수들을 사용하는 코드를 보여줍니다.

class NamedObj{
    string m_name;

public:

    NamedObj( const string& str) : m_name(str){
        cout << m_name << " object created\n";
    }

    ~NamedObj(){
        cout << m_name << " object destroyed\n";
    }

    string GetName(){	// NameObj에 접근 가능한지를 테스트하기 위해
        return m_name;
    }
};

int main(){

    shared_ptr<NamedObj> p1 = make_shared<NamedObj>("Apple");

    weak_ptr<NamedObj> wp(p1);
    // use_count
    cout << "use_count of Apple is " << wp.use_count() << endl;

    p1.reset(); // Apple객체 삭제
    // expired
    cout << "Apple was expired: " << boolalpha << wp.expired() << endl << endl;
    
    shared_ptr<NamedObj> p2 = make_shared<NamedObj>("Banana");
    wp = p2;    
	
    // lock
    if ( auto locked = wp.lock()){  // Banana 객체가 expired되지 않았으면 표현식은 true
        cout << locked->GetName() << " is not expired\n";
    }
}

▼출력

Apple object created
use_count of Apple is 1
Apple object destroyed
Apple was expired: true

Banana object created
Banana is not expired
Banana object destroyed

 

약한 참조수( weak count )

위에서 weak_ptr은 shared_ptr이 관리하는 객체의 대한 참조수를 증가시키지 않는다고 말했습니다.

그리고, 이 참조수는 첫 번째 shared_ptr이 생성될 때, 같이 생성되는 제어 블록( control block )에 저장됩니다.

 

이러한 제어 블록은 이뿐만이 아니라, 사용자 메모리 할당 객체( custom allocator )와 사용자 삭제 객체( custom deleter )와 같은 관리용 정보도 담고 있습니다.

 

그리고, 이러한 정보에는 약한 참조수( weak count )도 포함됩니다.

이 약한 참조수는 weak_ptr 객체의 개수를 저장하고 있는 숫자입니다.

 

그런데, 이 숫자는 왜 필요한 걸까요?

 

그것은 shared_ptr 포인터를 생성할 때 같이 할당한 제어 블록을 파괴하고, 이 제어 블록을 위해 할당되었던 메모리를 반환해야 할 때를 구하기 위해서입니다.

 

예를 들어, 모든 shared_ptr 객체가 모두 파괴되었다고 생각해 봅시다.

그럼, use_count ( shared_ptr이 관리하는 객체에 대한 참조수 )는 0이 되고, 이에 따라 shared_ptr이 관리했던 객체를 파괴합니다.

그런데, 이미 할당되어 있는 제어 블록은 아직 삭제할 수 없습니다.

왜냐하면, 아직 모든 weak_ptr 객체들이 파괴되지 않았을 수도 있기 때문입니다.

만약, 제어 블록에 대한 포인터를 갖고 있는 weak_ptr 객체가 남아있는 데, 제어 블록을 삭제하면 weak_ptr의 기능들은 제대로 정의되지 않게 될 것입니다.

따라서, 제어 블록을 삭제하고 싶으면, 모든 weak_ptr이 삭제된 후에야 가능하며, 이것을 알려주는 것이 약한 참조수( weak count )인 것입니다.

 

한 가지 더 생각해봐야 할 것이 있습니다.

make_shared 함수를 통해서 share_ptr을 생성하면, 같은 메모리 블록에 관리할 객체와 제어 블록이 연속되어 할당됩니다.

그러므로, 이 때는 use_count가 0이 되더라도, shared_ptr이 관리하는 객체가 바로 삭제되지 않을 수도 있습니다.

약한 참조수가 0이 아니라면, 같은 메모리 블록에 있는 제어 블록은 아직 삭제해선 안되기 때문입니다.

 

이런 경우엔 관리하던 객체와 제어 블록이 차지하던 메모리를 동시에 반환해야 하며, 이 때는 use_count와 약한 참조수 둘 모두 0이 될 때입니다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유