[EMC++] Item 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용해라
Item 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용해라
std::unique_ptr
를 설명하기에 앞서 생 포인터(raw pointer)와 스마트 포인터(smart pointer)에 대해서 간단하게 설명하겠다.
생 포인터(raw pointer)의 한계점
raw pointer는 우리가 지금까지 사용했던 일반적인 포인터를 이야기한다.
하지만 raw pointer는 다음과 같은 한계점들을 가지고 있다.
- 포인터가 하나의 객체를 가리키는지 배열을 가리키는지 구분하기 어렵다.
delete
와delete[]
중 뭘 써야 하는지 알기 어렵다.- 포인터를 사용한 후에 가리키는 객체를 삭제해야 하는지 알 수 없다. (포인터가 가리키고 있는 객체에 대한 소유권을 알 수 없다.)
- 파괴 방법을 알 수 없다. (
delete
를 사용해서 자원을 파괴해야 하는지 특정한 함수를 호출하여 자원을 파괴해야 하는지 알 수 없다.) - 파괴는 한 번만 하도록 신경 써야 한다. (중복 해제를 방지해야 한다.)
- 포인터가 가리키는 객체가 여전히 살아있는지 알 방법이 없다.
스마트 포인터(smart pointer)
이러한 raw pointer의 단점을 해결하기 위해 C++11부터는 smart pointer를 지원한다.
smart pointer는 raw pointer를 감싸는 wrapper 객체로 raw pointer의 기능을 대부분 지원하며 오류의 여지는 훨씬 적다.
smart pointer의 가장 큰 특징은 자원을 직접 해제하지 않아도 알아서 자원을 해제해 준다는 점이다.
smart pointer는 소멸할 때 자신이 가리키고 있는 데이터도 같이 파괴해서 자원 관리를 스택의 객체(포인터 객체)를 통해 수행할 수 있게 한다.
smart pointer의 종류는 std::auto_ptr
, std::unique_ptr
, std::shared_ptr
, std::weak_ptr
가 있다.
std::auto_ptr
는std::unique_ptr
의 완벽한 하위호환이라 C++11부터 비권장(deprecated)되었고 C++17부터 제거되었으므로 다루지 않겠다.
std::unique_ptr
std::unique_ptr
는 raw pointer와 크기와 성능이 거의 같으며 동일한 기능을 수행한다.
std::unique_ptr
의 특징은 특정 객체에 유일한 소유권(exclusive ownership)을 가지는 포인터 객체라는 점이다.
즉, 해당 객체를 가리키는 포인터는 하나만 존재할 수 있다.
std::unique_ptr
는 복사가 불가능하며 만약 다른 포인터에게 객체의 소유권을 넘겨주려면 이동이 필요하다.
소유권이 이전된 std::unique_ptr
를 댕글링 포인터(dangling pointer)라고 하며 이를 재참조하면 런타임 오류가 발생할 수 있다.
std::unique_ptr
는 계층 구조(hierachy) 안의 객체를 생성하는 팩토리 함수의 반환 형식으로 자주 사용한다.
투자(Investment) 부모 클래스를 상속받는 주식(stock), 채권(bond), 부동산(real estate) 자식 클래스들로 이루어진 계층 구조를 가정해보자.
class Investment { ... };
class Stock : public Investment { ... };
class Bond : public Investment { ... };
class RealEstate : public Investment { ... };
팩토리 함수는 일반적으로 heap에 객체를 생성하고 그 객체를 가리키는 포인터를 반환한다.
이때 반환 타입으로 std::unique_ptr
를 사용하면 자연스럽게 객체에 대한 소유권을 호출 대상에게 넘겨줄 수 있다.
// std::unique_ptr를 돌려주는 팩토리 함수
template <typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params);
{
// pInvestment의 타입은 std::unique_ptr<Investment>
auto pInvestment = makeInvestment( ... );
} // 스코프를 벗어나면 pInvestment가 가리키는 객체가 파괴된다.
std::unique_ptr의 custom deleter
기본적으로 자원 파괴는 delete
를 통해 일어나지만 custom deleter를 지정하여 파괴할 수도 있다.
custom deleter는 스마트 포인터 객체가 파괴될 때 호출되는 함수로 파괴할 객체를 가리키는 포인터를 인자로 받는다.
따라서 custom deleter를 통해 사용자는 원하는 작업을 수행하고 자원을 해제할 수 있다.
std::unique_ptr
는 raw pointer와 크기가 같지만 custom deleter를 사용할 경우 객체의 크기가 커진다.
이때 상태 없는 함수 객체(캡처 없는 람다 표현식)을 사용하면 객체의 크기 변화 없이 custom deleter를 사용할 수 있다.
// custom deleter (켑처 없는 람다 표현식)
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment);
delete pInvestment;
};
template <typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&&... params) {
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
if ( /* Stock 객체를 생성해야 하는 경우 */ ) {
pInv.reset(new Stock(std::forward<Ts>(params)...));
} else if ( /* Bond 객체를 생성해야 하는 경우 */ ) {
pInv.reset(new Bond(std::forward<Ts>(params)...));
} else if ( /* RealEstate 객체를 생성해야 하는 경우 */ ) {
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
custom deleter를 사용하여 std::unique_ptr
를 정의할 경우 반드시 명심해야 할 점이 있다. custom deleter도 타입에 포함된다는 점이다.
C++14부터는 함수의 리턴 타입 추론을 지원하므로 좀 더 간결하게 표현할 수 있다.
// C++14는 함수 리턴 타입의 추론을 지원
template <typename... Ts>
auto makeInvestment(Ts&&... params) {
// 함수 내부에서 커스텀 삭제자를 정의할 수 있음
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment);
delete pInvestment;
};
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
if ( /* Stock 객체를 생성해야 하는 경우 */ ) {
pInv.reset(new Stock(std::forward<Ts>(params)...));
} else if ( /* Bond 객체를 생성해야 하는 경우 */ ) {
pInv.reset(new Bond(std::forward<Ts>(params)...));
} else if ( /* RealEstate 객체를 생성해야 하는 경우 */ ) {
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
std::unique_ptr의 장점
std::unique_ptr
는 다음과 같은 장점이 있다.
std::uniqe_ptr는 가리키는 대상이 명확함
std::unique_ptr
는 개별 객체(std::unique_ptr<T>
) 혹은 배열 객체(std::unique_ptr<T[]>
)를 가리키는 두 가지 형태가 존재한다.
개별 객체 포인터(std::unique_ptr<T>
)는 인덱스 연산자(operator[]
)를 지원하지 않고 배열 객체 포인터(std::unique_ptr<T[]>
)는 역참조 연산자(operator→
, operator*
)를 지원하지 않는다.
따라서 어떤 객체를 가리키고 있는지 모호할 일이 없이 명확하다.
std::unique_ptr는 std::shared_ptr로 변환이 쉬움
std::unique_ptr
는 std::shared_ptr
로 변환이 쉽고 효율적이다.
따라서 std::unique_ptr
로 반환할 경우 클라이언트에서는 상황에 맞게 유연하게 구현할 수 있다.