[C++ 7.4.1] unique_ptr
1. unique_ptr 생성 방법
다음은 Simple 객체를 힙에 할당한 뒤 이를 해제하지 않아 메모리 누수가 현상이 발생하는 경우이다.
void leaky()
{
Simple* mySimplePtr = new Simple();
mySimplePtr->go();
}
코드를 작성할 때 동적으로 할당한 메모리를 제대로 해제한다고 여기기 쉽다.
하지만 그렇지 않을 가능성이 훨씬 높다. 다음 함수를 보자.
void couldBeLeaky()
{
Simple* mySimplePtr = new Simple();
mySimplePtr->go();
delete mySimplePtr;
}
이 함수는 Simple 객체를 동적으로 할당해서 사용하고 나서 delete를 호출한다.
이렇게 해도 메모리 누수가 발생할 가능성은 남아 있다.
go() 메서드에 익셉션(에러)이 발생하면 delete가 실행되지 않기 때문이다.
unique_ptr 구현
앞에서 본 두 코드 모두 unique_ptr로 구현해야 한다.
그러면 객체에 대해 delete를 직접 호출하지 않아도 된다.
unique_ptr 인스턴스가 스코프를 벗어나면 소멸자가 호출될 때 Simple 객체가 자동으로 해제된다.
void notLeaky()
{
auto mySimpleSmartPtr = make_unique<Simple>();
mySimpleSmartPtr->go();
}
이 코드는 C++14부터 제공하는 make_unique()와 auto 키워드를 동시에 적용했다.
그래서 Simple이라는 포인터 타입만 지정했다.
Simple 생성자에서 매개변수를 받는다면 make_unique() 호출문의 소괄호 사이에 지정하면 된다.
예를 들어 Simple(int, int)라면 make_unique<Simple>(1, 2);와 같이 생성자 인수를 전달할 수 있다.
make_unique()를 지원하지 않는 컴파일러를 사용한다면 다음과 같이 unique_ptr로 생성한다.
unique_ptr<Simple> mySimpleSmartPtr(new Simple());
C++ 이전에는 타입을 단 한 번만 지정하기 위해서 뿐만 아니라 안전을 위해 반드시 make_unique()를 사용해야 했다.
2. unique_ptr 사용 방법
스마트 포인터는 일반 포인터와 똑같이 *나 ->로 역참조 한다.
예를 들어 앞에서 본 예제에서 go() 메서드를 호출할 때 -> 연산자를 사용했다.
mySimpleSmartPtr->go();
다음과 같이 일반 포인터처럼 작성해도 된다.
(*mySimpleSmartPtr).go();
get() 메서드를 이용하면 내부 포인터에 직접 접근할 수 있다.
일반 포인터만 전달할 수 있는 함수에 스마트 포인터를 전달할 때 유용하다. 예를 들어 다음과 같은 함수가 있다고 하자.
void processData(Simple* simple) { /* 스마트 포인터를 사용하는 코드 */ }
그러면 이 함수를 다음과 같이 호출할 수 있다.
auto mySimpleSmartPtr = make_unique<Simple>();
processData(mySimpleSmartPtr.get());
reset()을 사용하면 unique_ptr의 내부 포인터를 해제하고, 필요하다면 이를 다른 포인터로 변경할 수 있다.
예를 들면 다음과 같다.
mySimpleSmartPtr.reset(); // 리소스 해제 후 nullptr로 초기화
mySimpleSmartPtr.reset(new Simple()); // 리소스 해제 후 새로운 Simple 인스턴스로 설정
release() 를 이용하면 unique_ptr와 내부 포인터의 관계를 끊을 수 있다.
release() 메서드는 리소시에 대한 내부 포인터를 리턴한 뒤 스마트 포인터를 nullptr로 설정한다.
그러면 스마트 포인터는 그 리소스에 대한 소유권을 잃으며, 리소스를 다 쓴 뒤 반드시 직접 해제해야 한다.
예를 들면 다음과 같다.
Simple* simple = mySimpleSmartPtr.release(); // 소유권을 해제한다.
// simple 포인터를 사용하는 코드
delete simple;
simple = nullptr;
unique_ptr은 단독 소유권을 표현하기 때문에 복사할 수 없다.
std::move() 유틸리티를 사용하면 하나의 unique_ptr를 다른 곳으로 이동할 수 있는데,
복사라기보다는 이동의 개념이다.
3. unique_ptr와 C 스타일 배열
unique_ptr 은 기존 C 스타일의 동적 할당 배열을 저장하는 데 적합하다.
예를 들어 정수 10개를 가진 C 스타일의 동적 할당 배열을 다음과 같이 표현할 수 있다.
auto myVariableSizedArray = make_unique<int[]>(10);
이렇게 unique_ptr로 C 스타일의 동적 할당 배열을 저장할 수 있지만,
이보다는 std::array나 std::vector와 같은 표준 라이브러리 컨테이너를 사용하는 것이 좋다.
4. 커스텀 제거자
기본적으로 unique_ptr은 new와 delete로 메모리를 할당하거나 해제한다.
하지만 다음과 같이 방식을 변경할 수 있다.
int* malloc_int(int value)
{
int* p = (int*)malloc(sizeof(int));
*p = value;
return p;
}
int main()
{
unique_ptr<int, decltype(free)*>myIntSmartPtr(malloc_int(42), free);
return 0;
}
이 코드는 malloc_int()로 정수에 대한 메모리를 할당한다.
unique_ptr은 메모리를 표준 free() 함수로 해제한다.