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

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

shared_ptr 이란

shared_ptr은 할당된 메모리를 자동으로 관리할 목적으로 C++ 11에 도입된 유틸리티 클래스입니다.

 

이 클래스를 사용하기 위해선 다음의 헤더 파일을 포함해야 합니다.

#include <memory>

 

다음은 shared_ptr의 기본 사용법입니다.

#include <iostream>
#include <memory>
using namespace std;

#define SIZE    100

class ResourceObj{
    int* m_pData;

public:
    ResourceObj(){
        m_pData = new int[SIZE];
        cout << "Resource Created\n";
    };

    ~ResourceObj(){
        delete [] m_pData;
        cout << "Resource Destory\n";
    };
};

int main(){

    shared_ptr< ResourceObj > ptr { new ResourceObj() };	// shared_ptr
    
    return 0;
}

▼출력

Resource Created
Resource Destory

shared_ptr 객체가 하는 일은 메모리에 생성한 ResourceObj 객체의 주소를 보관합니다.
그리고, shared_ptr 객체가 파괴될 때, 보관하고 있는 메모리 객체를 파괴하고 메모리를 자동으로 반환하는 것입니다.

 

이 기능은 unique_ptr이 수행하는 기능과 유사합니다.

그러나, 관리하고 있는 메모리 객체에 대한 소유권을 다루는 방식에 차이가 있습니다.

 

unique_ptr은 관리하고 있는 객체에 유일한 소유권만 인정합니다.

따라서, 한 unique_ptr이 객체를 소유하게 되면, 그 unique_ptr를 통해서만 메모리 객체에 접근할 수 있습니다.

 

unique_ptr에 관한 내용은 여기에 정리되어 있습니다.

 

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

unique_ptr 이란unique_ptr은 할당된 메모리를 자동으로 관리할 목적으로 C++ 11에 도입된 유틸리티 클래스입니다. 참고로, 이와 비슷한 스마트 포인터( smart pointer ) 클래스로 auto_ptr이 있었습니다.그러

codingembers.co.kr

 

반면, shared_ptr는 공동 소유권을 인정합니다.

그리고, 공동 소유권을 실현하는 방법으로, 할당된 메모리 객체에 대한 참조수를 관리합니다.

unique_ptr과 달리, shared_ptr A가 메모리 객체를 소유하고 있는 동안에도, 다른 shared_ptr B기 그 메모리 객체에 대한 소유권을 습득할 수 있습니다.

그 대신, 습득한 소유권에 대한 표식으로 참조수를 증가시킵니다.

 

만약, shared_ptr B가 범위( scope )를 벗어나 파괴되더라도, 관리하고 있는 메모리 객체에 대한 참조수가 감소될 뿐, 실제로 메모리 객체가 파괴되고 메모리가 반환되지 않습니다.

이 메모리 객체는 마지막으로 소유권을 가진 shared_ptr A마저 삭제되고, 참조수가 0이 되면 그때서야 파괴되고 메모리를 반환하게 됩니다.

 

이러한 소유권의 상태를 알 수 있게 하는 멤버 함수들이 uniqueuse_count입니다.

unique 함수는 shared_ptr이 가진 소유권이 유일한지를 검사합니다.

그리고, use_count 함수는 shared_ptr 객체들에 의해 공유된 소유권의 수( 참조수 )를 알려줍니다.

 

다음 코드에서 이러한 shared_ptr이 작동하는 과정을 볼 수 있습니다.

int main(){

    shared_ptr< ResourceObj > ptrA { new ResourceObj() };
    
    if ( ptrA.unique() ){	// 유일한 소유권을 가지고 있는지 확인
        cout << "ptrA has unique ownership\n";
    }

    {   // code block
        shared_ptr< ResourceObj > ptrB { ptrA };    // ptrA의 소유권 복사

    	// 현재의 참조수를 출력
        if ( ptrA.use_count() == ptrB.use_count() ){
            cout << "ptrA and ptrB have " << ptrA.use_count() << " use_count\n";
        }

        cout << "ptrB will be destoryed\n";
    }

    cout << "ptrA and has " << ptrA.use_count() << " use_count\n";

    cout << "ptrA will be destoryed\n";
    
    return 0;
}

▼출력

Resource Created
ptrA has unique ownership
ptrA and ptrB have 2 use_count
ptrB will be destoryed
ptrA and has 1 use_count
ptrA will be destoryed
Resource Destory

위에서 보듯이, ptrB는 복사 생성자를 통해서  ptrA의 소유권을 복사합니다.

그러나, ptrB가 삭제되더라도 ResourceObj 객체는 파괴되지 않습니다.

main 함수가 종료되고 ptrA가 삭제되고 나서야 ResourceObj이 파괴됩니다.

 

제어 블록( control block )

공유 소유권을 가능하게 하는 것은 할당하는 객체에 대한 참조수를 관리하는 동적 객체가 있기 때문입니다.

이 동적 객체를 제어 블록( control block )라고 합니다.

 

그래서, 다음 코드는 실제로 메모리를 두 번 할당합니다.

ResourceObj 객체와 제어 블록을 위해서입니다.

shared_ptr< ResourceObj > ptr { new ResourceObj() };

 

그렇기 때문에, 다음 코드는 문제를 발생시킵니다.

int main(){

    ResourceObj* pRes = new ResourceObj();
    shared_ptr< ResourceObj > ptrA { pRes };    // pRes 관리

    {   // code block
        shared_ptr< ResourceObj > ptrB { pRes };    // 같은 pRes 관리

        cout << "ptrB will be destoryed\n";
    }

    cout << "ptrA will be destoryed and Program will be crashed !!\n";
    
    return 0;
}

이 코드에서, ptrB는 ptrA의 제어 블록의 존재를 알 수 없습니다.

그러므로, ptrB가 가진 제어블록의 참조수는 1이 됩니다.

( 먼저 생성된 ptrA의 제어블록의 참조수도 마찬가지로 1입니다. )

 

따라서, ptrB는 범위( scope )를 벗어나면서 pRes를 파괴하고, main 함수 종료 시, ptrA도 같은 pRes를 파괴하려고 합니다.

안타깝지만 예외가 발생합니다.

 

shared_ptr을 제대로 사용하려면, ptrA 객체로부터 ptrB를 생성해서 같은 제어 블록을 사용해야 합니다.

shared_ptr< ResourceObj > ptrB { ptrA };    // ptrA의 소유권 복사

 

그리고, std::make_shared 함수를 사용하면 이런 문제는 발생할 수 없습니다.

 

std::make_shared 함수

make_shared 함수는 C++14에 도입된, shared_prt 객체를 사용하기 쉽도록 만들어 주는 함수입니다.
이 함수는 객체를 할당할 뿐만 아니라, 객체를 구성하기 위한 매개 변수 생성자를 통해 객체를 초기화할 수도 있습니다.

 

이 방식은 메모리 객체를 직접 할당하지 않기 때문에, 이전 항목과 같은 실수를 방지할 수 있습니다.

이전 항목의 코드는 다음과 같이 변경할 수 있습니다.

int main(){
    
    // make_shared
    shared_ptr< ResourceObj > ptrA = make_shared<ResourceObj>();   

    {   // code block
        shared_ptr< ResourceObj > ptrB { ptrA };    // 같은 ResourceObj 관리

        cout << "ptrB will be destoryed\n";
    }

    cout << "ptrA will be destoryed\n";
    
    return 0;
}

이제 ptrB가 ResourceObj에 대한 소유권을 얻으려면, ptrA를 거치는 수밖에 없습니다.

 

그리고, 이 함수는 메모리 객체와 제어 블록( control block )을 연속된 메모리에 할당하기 때문에, 직접 생성한 shared_ptr객체보다 이 함수를 통해 생성된 shared_ptr 객체의 속도가 훨씬 빠릅니다.

 

shared_ptr 사용 시 고려할 사항

shared_ptr은 무척 편리한 클래스입니다.

multi-thread 환경에서도 일단 shared_ptr 객체를 작성해서, 필요할 때마다 자유로이 복사해서 사용하고, 

사용이 끝나더라도, 관리하는 객체의 안전성을 보장받을 수 있습니다.

 

그렇지만, 그에 대한 대가는 비쌉니다.

shared_ptr의 소유권을 공유하기 위해서, 참조수를 관리하는 동안 절대 방해받지 않는, 비용이 큰 연산( 원자적 연산 atomic operation )을 수행해야 합니다.

 

다음 예제를 보겠습니다.

void DoSomething(shared_ptr< ResourceObj > ptr, int num){
    
    // Do Something with ResourceObj
    
    // ptr이 파괴되면서 ResourceObj에 대한 참조수 감소
}

int main(){
    
    // make_shared
    shared_ptr< ResourceObj > ptr = make_shared<ResourceObj>();   

    for( int i = 0; i < 10000; i++){
        DoSomething( ptr, i);	// ResourceObj에 대한 참조수 증가
    }

    return 0;
}

이 예제에선 만 번에 걸쳐, ResourceObj에 대한 참조수를 증가시켰다가 감소시킵니다.

이 과정은 눈에 보이지 않기 때문에, 이 점에 대해 의식하고 있지 않다면 다른 곳에서 성능 문제가 발생하고 있다고 생각할 수도 있습니다.

 

따라서, 먼저 소유권을 반드시 공유할 필요가 있는지 생각해 봐야 합니다.

// ResouceObj에 대한 포인터를 직접 사용
void DoSomething_withResourceObj(ResourceObj* pRes, int num){
    
    // Do Something with ResourceObj
}

int main(){
    
    // make_shared
    shared_ptr< ResourceObj > ptr = make_shared<ResourceObj>();   

    for( int i = 0; i < 10000; i++){
    	DoSomething_withResourceObj( ptr.get(), i);		// 불필요한 소유권 공유 대안
    }

    return 0;
}

위 예제에서는 단순히 할당한 메모리 객체를 가리키는 포인터를 함수 인자로 전달하는 방법으로도 목적을 달성할 수 있다는 것을 보여줍니다.

 

그리고, 공유가 전혀 필요 없는 메모리 객체를 할당하려면 shared_prt 대신 unique_ptr을 사용하는 방법도 있습니다.

 

또한, shared_ptr은 순환 참조( circular reference ) 문제를 스스로 해결하지 못합니다.

이것은 참조수 관리에 관한 문제로, C++은 이 문제를 해결하기 위한 방법으로 std::weak_ptr 객체를 도입했습니다.

이에 관한 내용은 다음 글에서 볼 수 있습니다.

 

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

weak_ptrweak_ptr은 shared_ptr를 지원할 목적으로 C++11에 도입된 클래스입니다. 따라서, 이 글을 이해하기 위해선 shared_ptr의 사전 지식이 필수입니다.shared_ptr에 관한 내용은 여기에서 볼 수 있습니다. 

codingembers.co.kr

 

 

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