Computing

스마트 포인터 정리 (Smart Pointer, unique_ptr, shared_ptr) 본문

Programming/C++

스마트 포인터 정리 (Smart Pointer, unique_ptr, shared_ptr)

jhson989 2023. 8. 19. 14:42

RAII와 동적 메모리 자원 관리

이전글에서 C++에서 자원 관리를 용이하게 해주는 RAII 디자인 패턴에 대해서 정리하였다. RAII 디자인 패턴은 자원의 생애 주기 (자원 할당 -> 자원 사용 -> 자원 해제) 를 객체의 생애 (객체의 Constructor 호출 -> 객체의 Destructor 호출) 에 바인드하여 자원 관리를 C++ 런타임에서 자동으로 맡기는 기법이다.

 

RAII 디자인 패턴을 달성하기 위해 만들어진 대표적인 class로는 std::unique_ptr, std::shared_ptr, std::lock_guard가 있다. 이때, std::unique_ptr, std::shared_ptr은 Smart pointer (스마트 포인터) 라고도 한다. 스마트 포인터는 RAII 패턴을 이용해 동적 메모리 관리를 프로그래머가 아닌 C++ 런타임이 관리하도록 만들어진 클래스이다. 즉 따라서 스마트 포인터가 선언되면 자원이 할당되고, 스마트 포인터가 삭제 (= 스마트 포인터가 선언된 scope를 나가면) 되면 자원이 자동 해제된다. 그렇기 때문에 스마트라는 이름이 붙지 않았나 싶다.

 

이번 글에서는 스마트 포인터인 std::unique_ptr과 std::shared_ptr에 대해서 정리하고자 한다. 간단히 두개를 비교 정리하자면 다음과 같다.

 

  • std::unique_ptr : 하나의 동적 할당된 객체를 관리하는 스마트 포인터로, 해당 객체의 유일한 소유권을 가진다. 객체의 소유권을 가진 std::unique_ptr 변수가 소멸되면 해당 객체는 해제된다.
  • std::shared_ptr : 하나의 동적 할당된 객체를 관리하는 스마트 포인터로, 해당 객체는 여러 std::shared_ptr에 의해서 소유권이 공유된다. 객체의 소유권을 가진 모든 std::shared_ptr 변수들이 소멸되면 해당 객체는 해제된다.

 

std::unique_ptr, std::shared_ptr 모두 RAII 디자인 패턴에 따라 동적 할당된 객체를 자동 관리하는 클래스이다. 다만 차이는, 해당 객체를 유일하게 소유하는 지 혹은 소유권을 공유할 수 있는 지이다. 소유권 (ownership) 이라는 개념이 처음에는 와닿지 않았는데, 간단히 자원의 소유권을 가진 스마트 포인터는 해당 자원의 해제를 책임진다고 생각하면 좋을 듯하다. std::unique_ptr은 유일하게 (unique) 자원의 소유권을 가지기에 std::unique_ptr 하나가 배타적으로 해당 자원의 해제를 책임지게 된다.

 

 

 

std::unique_ptr

std::unique_ptr은 스마트 포인터의 일종으로, 동적 메모리 할당된 객체의 소유권을 가진다. 따라서 std::unique_ptr 변수가 소멸되거나 새로운 객체의 소유권을 얻으면, 기존 객체를 자동 소멸시키고 동적 메모리를 해제한다.

 

std::unique_ptr은 하나의 동적 할당된 객체를 소유하거나, 동적 할당된 객체들의 배열을 소유할 수 있다.

unique_ptr<int> int_single(new int(3));
unique_ptr<int[]> int_array(new int[10]);
// T는 임의의 객체
std::unique_ptr<T> t_single(new T); // 동적 할당된 T 객체 하나를 소유함
std::unique_ptr<T[]> t_array(new T[10]); // 동적 할당된 T 객체 배열을 소유함

 

std::unique_ptr은 MoveConstructible, MoveAssignable의 조건은 만족하나, CopyConstructible, CopyAssignable 조건을 만족하지 않는다. 즉 복사 생성자와 복사 할당 연산자는 지원하지 않고 deleted function[3]으로 정의되어 있다. 그에 비해 이동 생성자와 이동 할당 연산자는 지원한다.

 

생각해보면 당연한 이유인데, std::unique_ptr 하나는 하나의 자원의 소유권을 유일하게 가지고 있어야 한다. 만약 std::unique_ptr가 복사가 된다면 복사한 std::unique_ptr도 자원의 소유권을 공유하게 된다. 따라서 이를 원천 방지하기 위해 std::unique_ptr은 복사 생성자와 복사 할당 연산자를 deleted function으로 정의하여 사용하지 못하게 하였고, 혹시나 실수에 의해 사용되더라도 컴파일러단에서 error로 캐치할 수 있도록 하였다.

 

반면에, 이동 생성자 및 이동 할당 연산자는 지원하기에, 이동에 의한 다른 std::unique_ptr 생성은 가능하도록 하였다. 이동 생성자는 기존 std::unique_ptr이 가지고 있던 자원의 소유권을 새로운 std::unique_ptr로 전달하므로 사용 가능하다.

 

unique_ptr<T> ptr1(new T);       // 가능함
unique_ptr<T> otherPtr1 = ptr1; // Error: 복사 생성자 불가

unique_ptr<T> ptr2(new T);                  // 가능함
unique_ptr<T> otherPtr2 = std::move(ptr2); // 가능함. 단 ptr2는 더 이상 사용 불가능

 

std::shared_ptr은 관리하고 있는 동적 할당된 객체의 pointer (이를 a pointer to managed object라고 함) 뿐만 아니라, control block을 가짐으로서 소유권의 공유가 가능해진다. control_block은 하나의 자원을 소유하는 std::shared_ptr이 생성될 때 최초 생성된다. 이후 해당 자원을 공유하는 std::shared_ptr이 추가될 때마다, 새로운 std::shared_ptr은 이 control_block도 같이 참조한다. control_block에는 다양한 정보가 저장되는데, 특히 managed object를 참조하고 있는 std::shared_ptr의 개수를 저장해둔다. manged object를 참조하는 std::shared_ptr이 추가될수록 개수는 증가되고, 소멸될수록 개수는 감소한다. 개수가 0이 되면 managed object는 자동 소멸된다.

 

 

 

std::shared_ptr

std::shared_ptr은 스마트 포인터의 일종으로, 동적 메모리 할당된 객체의 소유권을 가진다. 다만 std::unique_ptr과의 차이점은 메모리 자원의 소유권을 여러 std::shared_ptr이 공유하여 소유할 수 있다는 것이다. 따라서 하나의 자원의 소유권을 공유한 모든 std::shared_ptr 변수가 소멸되면, 기존 객체는 자동 소멸되고 동적 메모리가 해제된다.

 

std::shared_ptr은 하나의 동적 할당된 객체를 소유하거나, 동적 할당된 객체들의 배열을 소유할 수 있다.

std::shared_ptr<int> int_single(new int(3));
std::shared_ptr<int[]> int_array(new int[10]);
// T는 임의의 객체
std::shared_ptr<T> t_single(new T); // 동적 할당된 T 객체 하나를 소유함
std::shared_ptr<T[]> t_array(new T[10]); // 동적 할당된 T 객체 배열을 소유함

 

std::shared_ptr은 MoveConstructible, MoveAssignable, CopyConstructible, CopyAssignable 조건을 만족한다. 즉 복사 생성자, 복사 할당 연산자, 이동 생성자, 이동 할당 연산자 모두를 지원한다. 복사가 지원됨에 따라 하나의 자원은 여러std::shared_ptr들에게 복사되어 공유될 수 있다.

    std::shared_ptr<T> ptr1(new T);       // 가능함
    std::shared_ptr<T> otherPtr1 = ptr1; // 이것도 가능함. 하나의 자원을 ptr1, otherPtr1이 동시에 소유함
    
    std::shared_ptr<T> ptr2(new T);                  // 가능함
    std::shared_ptr<T> otherPtr2 = std::move(ptr2); // 가능함. 단 ptr2는 더 이상 사용 불가능

 

std::shared_ptr은 구체적으로 control block이라는 동적 할당된 object를 통해 한 자원에 대한 소유권의 공유를 지원한다. control block은 한 자원을 소유하고 있는 std::shared_ptr의 개수를 저장하고 있다. 이 control block은 한 자원을 참조하는 std::shared_ptr 사이에 공유되는데, 자원을 참조하는 std::shaerd_ptr이 추가될 때마다 개수를 증가시킨다. 마찬가지로 자원을 참조하는 std::shared_ptr이 소멸될 때마다 개수는 감소된다. 따라서 개수가 0이 되면 자원은 더 이상 참조되지 않는다는 것을 알 수 있고 자원은 자동 해제된다.

 

 

 

Reference

[1] https://en.cppreference.com/w/cpp/memory/unique_ptr

[2] https://en.cppreference.com/w/cpp/memory/shared_ptr

[3] https://www.ibm.com/docs/en/i/7.3?topic=definitions-deleted-functions-c11