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 객체의 소유권 상태를 알 수 있게 하는 멤버 함수들이 unique와 use_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의 소유권을 공유하는 것은 비용이 비싼 연산입니다.
'C, C++ > 표준 라이브러리' 카테고리의 다른 글
[C++] 우선순위 큐( std::priority_queue )의 활용법 (0) | 2025.04.25 |
---|---|
[C++] shared_ptr 클래스를 지원하기 위한 std::weak_ptr (0) | 2025.04.10 |
[C++] 독점적으로 자원을 관리하는 std::unique_ptr 객체 (0) | 2025.03.29 |
[C++] 내부 삽입을 통해 원소를 추가할 수 있는 std::emplace (0) | 2025.03.27 |
[C++] 여러 개의 값을 저장할 수 있는 std::tuple (0) | 2025.03.26 |