[C++] 자원을 공유하는 std::shared_ptr 객체

반응형

std::shared_ptr

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

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

#include <memory>

 

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

#include <iostream>
#include <memory>

#define SIZE    100

class ResourceObj{	// 자원 클래스
    int* m_pData;

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

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

int main(){

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

▼출력

Resource Created
Resource Destory

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

이러한 기능은 std::unique_ptr이 수행하는 기능과 유사합니다.
그러나, 관리하고 있는 메모리 객체에 대한 소유권을 다루는 방식에 차이가 있습니다.

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

 

[C++] 독점적으로 자원을 관리하는 std::unique_ptr 객체

std::unique_ptrstd::unique_ptr은, C++ 11에, 할당된 메모리를 편리하게 관리할 목적으로 도입된 스마트 포인터( smart pointer ) 클래스입니다. 이 unique_ptr 객체가 하는 일은, 메모리가 할당된 객체의 주소를

codingbonfire.tistory.com

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

 

반면, shared_ptr공동 소유권( shared ownership )을 인정합니다.
그리고, 이 공동 소유권을 실현하는 방법으로, 할당된 메모리 객체에 대한 참조수( reference count )를 관리합니다.

그래서, unique_ptr 객체와 달리, shared_ptr 타입의 A 객체가 어떤 메모리 객체를 소유하고 있는 동안에도, 다른 shared_ptr B 객체도 그 메모리 객체에 대한 소유권을 습득할 수 있습니다.
그리고, 추가로 습득한 소유권에 대한 표식으로 참조수를 증가시킵니다.

그리고, 만약 shared_ptr B가 객체의 범위( scope )를 벗어나 파괴되더라도, 관리하고 있던 메모리 객체에 대한 참조수가 감소될 뿐, 실제로 이 메모리 객체가 파괴되지는 않습니다.
이 메모리 객체는, 마지막으로 소유권을 가진 shared_ptr A 객체마저 삭제되고, 그래서 참조수가 0이 될 때야, 파괴되고 시스템에 메모리를 반환하게 됩니다.

이러한 shared_ptr 객체의 소유권 상태를 알 수 있게 하는 멤버 함수들이 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";
}

▼출력

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

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

( 참고로, unique_ptr 클래스는 , =delete 지시자를 통해서, 복사 생성자를 삭제했습니다. )

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

아직, ResourceObj 객체에 대한 참조수가 0이 아니기 때문입니다.

이 메모리 객체는, main 함수가 종료될 때, ptrA가 삭제되고 나서야 파괴됩니다.

 

제어 블록( control block )

shared_ptr 객체의 공유 소유권을 가능하게 하는 것은, 이 객체가 관리하고 있는 메모리 객체에 대한 참조수( reference count )를 관리하는 동적 객체가 따로 있기 때문입니다.
이 동적 객체를 제어 블록( control block )라고 합니다.

그리고, 이를 위해서 shared_ptr 객체를 생성하는 다음 코드는 실제로 메모리를 두 번 할당합니다.

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

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";
}

위에서, ptrB 객체는 ptrA의 제어 블록의 존재를 알 수 없고, 접근할 수도 없습니다.
그러므로, ptrB가 가진 제어블록의 참조수는 1이 됩니다.
( 먼저 생성된 ptrA의 제어블록의 참조수도 마찬가지로 1입니다. )

따라서, ptrB는 범위( scope )를 벗어나면서, 관리하고 있던, pRes를 파괴하고 합니다.

그리고, main 함수 종료 시, ptrA도 같은 ( 이미 파괴된 ) pRes 객체를 다시 파괴하려고 시도합니다.
안타깝게도, 여기서 예외가 발생합니다.

 

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

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

 

그리고, C++ 표준 라이브러리가 제공하는, 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";
}

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

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

 

 

shared_ptr 사용 시 고려할 사항

shared_ptr은 무척 편리한 클래스입니다.
multi-thread 환경에서도 일단 shared_ptr 객체를 작성하면, 필요할 때마다 자유로이 복사해서 사용할 수 있고, 사용이 끝나더라도, 관리하는 객체의 안전성을 보장받을 수 있습니다.

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

 

그래서, 이러한 shared_ptr 객체를 사용하는 것은, 이 객체가 관리하고 있는 객체를 직접 사용하는 것보다 훨씬 느립니다.

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에 대한 참조수 증가
    }
}

이 예문에선 DoSomething 함수에 shared_ptr 객체를 값으로 전달( call by value )하더라도, 관리되는 ResourceObj 객체가 복사되거나 파괴되는 일은 일어나지 않습니다.

하지만, 이 과정에서 통해서, ptr 객체의 ResourceObj에 대한 참조수를, 만 번에 걸쳐, 증가시켰다가 감소시켜야 합니다.

이것은 꽤나 큰 비용의 낭비로, 이 점에 대해 의식하고 있지 않다면, 다른 곳에서 성능 문제가 발생하고 있다고 착각할 정도입니다.

따라서, shared_ptr 객체를 사용할 때는, 우선 이 객체의 소유권을 반드시 공유할 필요가 있는지 생각해 봐야 합니다.

// 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);		// 불필요한 소유권 공유 대안
    }
}

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

 

참고로, DoSomething 함수가 shared_ptr<ResourceObj>& 타입의 매개변수 사용해도, 소유권이 복사되는 것을 막을 수는 있지만, 이 경우는 shared_ptr 클래스를 반드시 사용해야 하기 때문에, 위의 DoSomething_withResourceObj 함수보다는 제한되는 사용 범위를 갖게 됩니다.

그리고, 이런 이유 때문에, 공유가 전혀 필요 없는 메모리 객체를 할당하려면, shared_prt 대신 unique_ptr을 사용하는 것을 고려하는 것이 좋습니다.

 

또한, shared_ptr은 순환 참조( circular reference ) 문제를 스스로 해결하지 못합니다.
이것은 참조수 관리에 관한 문제로, shared_ptr 객체가 관리하는 메모리 객체가 삭제되지 않는 문제입니다.

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

 

[C++] shared_ptr 클래스를 지원하기 위한 std::weak_ptr

std::weak_ptrstd::shared_ptr은 편리한 스마트 포인터( smart pointer )입니다. 그렇지만, 순환 참조( circular reference )의 문제를 자체적으로 해결하지 못하는 구조적인 약점이 있습니다.그래서, 외부에서 도

codingbonfire.tistory.com

 

 

정리

std::shared_ptr 객체는 메모리 객체를 공동으로 관리할 수 있는 스마트 포인터( smart pointer )입니다.

같은 메모리 객체를 관리하는 모든 shared_ptr 객체가 파괴될 때까지, 이 메모리 객체는 파괴되지 않습니다.

make_shared 함수를 사용하면, std::shared_ptr 객체를 사용하면서 생길 수 있는 실수를 방지할 수 있습니다.

shared_ptr의 소유권을 공유하는 것은 비용이 비싼 연산입니다.

 

 

 

 

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