Effective Modern C++ #18 소유권 독점 자원의 관리에는 std::unique_ptr 를 사용하라
Smart Pointer
raw pointer는 아래와 같은 이유 때문에 좋아할 수가 없다.
- 선언만으로는 객체를 가리키는지 배열을 가리키는지 알 수 없음
- 포인터 사용이 완료된 뒤, 가리키는 대상을 직접 파괴해야하는지 알 수 없음 (소유 여부 모름)
- 가리키는 대상을 직접 파괴해야한다는 것을 알아도, 어떻게 파괴하는지 모름
- delete 로 파괴해야함을 알아도, 1번 이유로 delete, delete[] 어떤 것 사용해야하는지 모름
- 포인터가 가리키는 대상 소유 여부를 알고 파괴법도 알아도, 모든 경로에서 1번만 파괴되는지 보장하기 어려움
- 포인터가 가리키는 대상을 잃었는지 아는법 없음 (가리키는 대상이 유효한지, 파괴되었는지 모름)
smart pointer (raw pointer의 wrapper) 를 사용해서 이러한 문제를 해결할 수 있다.
동적으로 할당된 객체가 문제없이 파괴되도록 보장함으로써 memory leak 이 발생하지 않게 설계되어 있다.
스마트 포인터는 4종류가 있다.
- std::auto_ptr
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
std::auto_ptr 는 C++98 에서 사용했던 과거의 smart pointer로, 비권장 기능이며 C++11 이상부터는 사용할 필요 없다.
18. 소유권 독점 자원의 관리에는 std::unique_ptr 를 사용하라
smart pointer 를 사용하기로 결정했다면, 가장 먼저 옵션으로 생각해야할 것은 std::unique_ptr 이다.
raw pointer 와 유사한 크기를 가졌고, 동일한 명령을 실행한다.
따라서 컴퓨팅 환경이 넉넉하지 않은 곳에서도 사용 가능하다.
std::unique_ptr 는 자원에 대해 독점적 소유권을 가진다. (항상 자신이 가리키는 객체를 소유함)
- 포인터 간 이동 연산
- 허용됨
- 원본 → 대상으로 포인터가 옮겨짐
- 포인터 간 복사 연산
- 허용되지 않음
- 동일한 객체를 두 포인터가 가리키게 되기 때문 ('독점적 소유권'이라는 정의와 맞지 않음)
따라서 이동 연산만 사용가능한 move-only type 이다.
std::unique_ptr는 팩토리 함수의 리턴 타입으로 사용하기 적합하다.
팩토리 함수가 반환하는 자원은 일반적으로 호출자가 독점적으로 소유하기 때문이다.
팩토리 함수 예시)
template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
std::unique_ptr 는 소유권을 이전하는 시나리오에서도 활용된다.
소유권 이전 시 발생할 수 있는 아래 두 케이스 모두 안전하게 자원을 관리한다.
- 소유권 이전 성공
- 특정 객체로 std::unique_ptr 가 이동한 경우, 그 객체가 파괴될 때 std::unique_ptr 의 소멸자에 의해 자원도 파괴됨
- 소유권 이전 실패
- exception 등 동작으로 소유권을 잃어도, std::unique_ptr 의 소멸자가 호출되어 관리 자원이 파괴됨
Custom Deleter (커스텀 삭제자)
보통 std::unique_ptr 의 소멸자는 delete 를 통해 파괴한다.
목적에 따라 커스텀 삭제자를 사용하여, 파괴하기 전에 로그를 기록하는 등의 구현도 가능하다.
이 때 std::unique_ptr 의 템플릿 둘째 형식 인수에 커스텀 삭제자의 형식을 지정해야한다.
커스텀 삭제자를 사용하는 경우, 호출자 입장에서 파괴할 때 해야하는 특별한 처리에 대해 몰라도 되는 장점이 있다.
커스텀 삭제자 예시)
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 {
// 다른 객체별 케이스
}
return pInv
}
예시의 makeInvestment 함수는 아래와 같은 동작으로 구성된다.
- 커스텀 삭제자를 사용하는 null std::unique_ptr 생성
- 조건에 따라 std::unique_ptr가 적절한 형식의 객체 가리키게 함
- 객체 가리키는 std::unique_ptr 리턴
(2번) new 키워드로 생성한 객체로 raw pointer 를 std::unique_ptr 에 배정할 수 없다.
raw pointer → smart pointer 암묵적 변환은 성립되지 않기 때문이다.
따라서 reset 멤버함수를 사용해 pInv에 신규 객체의 소유권을 부여한다.
std::unique_ptr는 기본 삭제자(delete) 를 사용할 때, raw pointer 와 크기가 유사하다.
하지만 함수 객체 커스텀 삭제자를 사용하는 경우,객체 크기만큼 std::unique_ptr의 크기가 증가한다.
캡쳐 없는 람다표현식과 같은 stateless function object의 경우 std::unique_ptr의 크기 변화가 없다.
따라서 위 예시처럼 람다 표현식을 커스텀 삭제자로 사용하는 것이 적절하다.
아래의 raw pointer 의 단점은 std::unique_ptr에서 직관적으로 해결된다.
- delete 로 파괴해야함을 알아도, 1번 이유로 delete, delete[] 어떤 것 사용해야하는지 모름
std::unique_ptr는 객체 대상(`std::unique_ptr`), 배열 대상(`std::unique_ptr<t[]></t[]>`)으로 나뉘기 때문이다.
형태가 다르므로, std::unique_ptr 가 어떤 객체 가리키는지 알 수 없는 문제는 발생하지 않는다.
참고로 배열용 std::unique_ptr는 굳이 사용할 필요가 없다.
내장 배열보다는 std::array, std::vector 등 stl 이 더 나은 선택이기 때문이다.
마지막으로 unqiue_ptr는 std::shared_ptr 로 변환도 쉽고 효율적이다.
std::shared_ptr<Investment> sp = makeInvestment(params);
이러한 특성으로 팩토리 함수로 사용하기 더 적절하다.
호출할 때 자원 소유권에 대한 선택을 유연하게 변경할 수 있기 때문이다.