C++/Effective Modern C++

[Effective Modern C++] Smart pointer : std::unique_ptr<>

ruru14 2022. 5. 9. 17:10

스마트 포인터란 Raw pointer를 감싸는 래퍼(Wrapper) 클래스 입니다.

기존의 포인터가 가진 몇 가지 문제점들을 회피하면서, 포인터의 기능을 제공합니다.

 

이번 글에서는 스마트 포인터의 한 종류인 std::unique_ptr<>에 대하여 살펴보도록 하겠습니다.

 


 

1. std::unique_ptr<>

 

더보기

std::unique_ptr은 스마트 포인터를 사용할 때 가장 우선적으로 고려되는 포인터 입니다.

기본적으로 대부분의 연산에서 Raw pointer와 같은 명령을 수행합니다.

이것은 메모리 및 CPU의 사용에서 Raw pointer에 비해 무겁지 않음을 의미하고, Raw pointer를 사용할 때의 비용과 거의 동일하다는 뜻이 됩니다.

 

선언 및 사용은 아래와 같이 할 수 있습니다.

#include <iostream>
#include <memory>

int main
    int a = 10;
    int* b = &a;
    std::unique_ptr<int> c(new int(a));

    std::cout << *b << "\n";
    std::cout << *c << "\n";
}
10
10

 

이후 문단들에서 std::unique_ptr의 특징에 대하여 살펴보도록 하겠습니다.

 

2. 독점적 소유권

 

더보기

std::unique_ptr은 포인팅 하는 대상에 대한 독점적 소유권(Exclusive ownership)을 가지고 있습니다.

NULL이 아닌 std::unique_ptr은 항상 자신이 가리키는 객체를 소유하고 있으며, 이 소유권은 다른 포인터와 공유되지 않습니다.

독점적인 소유권을 가져야 하기 때문에, std::unique_ptr은 복사 연산을 허용하지 않는 이동 전용 형식 입니다.

#include <iostream>
#include <memory>

int main(){
    std::unique_ptr<Integer> a(new Integer(10));
    std::unique_ptr<Integer> b = a;
    std::unique_ptr<Integer> c(a);
}
C2280 : 'std::unique_ptr<>(&)': 삭제된 함수를 참조하려고 합니다.

위와 같이 복사 배정 연산자, 혹은 복사 생성자를 통해 std::unique_ptr을 생성하려 할 경우 컴파일이 실패합니다.

 

3. Delete, custom deleter

 

더보기

std::unique_ptr은 소멸 시 자신이 가리키는 자원을 파괴합니다.

이동 연산의 경우, 이동 후 기존 std::unique_ptr은 nullptr로 설정됩니다.

std::unique_ptr의 파괴 작업은 내부적으로 가리키고 있는 Raw pointer에 delete연산을 적용하는 방식으로 수행됩니다.

이 때, delete연산 이외에 특정 작업이 필요할 수 있습니다.

이러한 경우를 위해 std::unique_ptr은 커스텀 삭제자를 사용할 수 있도록 할 수 있습니다.

#include <iostream>
#include <memory>

class Integer {
public:
    Integer(int val) : data(val) { }
    virtual ~Integer();
private:
    int data;
};

int main() {
    auto del_int = [](Integer* p_int) {
        std::cout << "Delete Integer\n";
        delete p_int;
    };

    std::unique_ptr<Integer, decltype(del_int)> a(new Integer(10), del_int);
}
Delete Integer

커스텀 삭제자를 사용할 경우, std::unique_ptr에 그 형식과 해당 함수를 지정해야 합니다.

지정된 함수는 해당 객체가 소멸되는 시점에 호출됩니다.

 

4. std::unique_ptr<T[]>

 

더보기

std::unique_ptr<>의 형태는 두 가지 입니다.

첫 번째는 개별 객체를 위한 std::unique_ptr<T>이고, 두 번째는 배열을 위한 std::unique_ptr<T[]>입니다.

지금은 비 권장 기능인 초기형태의 스마트 포인터인 std::auto_ptr<>은 배열의 해제에 관련된 이슈가 존재했는데, std::unique_ptr은 객체의 형태를 단일 객체와 배열로 세분화하면서 이러한 문제를 해결했습니다.

 

두 객체는 단일 객체인지, 배열인지의 차이가 존재하기 때문에 일부 특정 연산이 제한되어 있습니다.

예를 들어 단일 객체의 std::unique_ptr<T>은 operator[]를 제공하지 않으며, 배열 객체인 std::unique_ptr<T[]>은 역참조 연산 (operator*및 operator->를 제공하지 않습니다.

 

하지만 배열 형식으로 포인터를 관리하는 데 std::unique_ptr<T[]>는 권장되지 않습니다.

기본적으로 사용되는 템플릿이 내장 배열이라는 점에서, std::vector<>혹은 std::array<>등과 같은 STL의 배열이 나은 경우가 많기 때문입니다.

 


 

std::unique_ptr은 다른 스마트 포인터인 std::shared_ptr로의 유연한 변환이 가능합니다.

std::shared_ptr은 이름 그대로 공유가 가능한 스마트 포인터로, std::unique_ptr과는 비슷하지만 여러 부분에서 다른 특징을 가지고 있습니다.

이에 관해서는 다음 글에서 살펴보도록 하겠습니다.

 

감사합니다.