Item 18. 소유권 독점 자원의 관리에는 std::unique_ptr를 사용해라

std::unique_ptr를 설명하기에 앞서 생 포인터(raw pointer)와 스마트 포인터(smart pointer)에 대해서 간단하게 설명하겠다.


생 포인터(raw pointer)의 한계점

raw pointer는 우리가 지금까지 사용했던 일반적인 포인터를 이야기한다.

하지만 raw pointer는 다음과 같은 한계점들을 가지고 있다.

  1. 포인터가 하나의 객체를 가리키는지 배열을 가리키는지 구분하기 어렵다.
  2. deletedelete[] 중 뭘 써야 하는지 알기 어렵다.
  3. 포인터를 사용한 후에 가리키는 객체를 삭제해야 하는지 알 수 없다. (포인터가 가리키고 있는 객체에 대한 소유권을 알 수 없다.)
  4. 파괴 방법을 알 수 없다. (delete를 사용해서 자원을 파괴해야 하는지 특정한 함수를 호출하여 자원을 파괴해야 하는지 알 수 없다.)
  5. 파괴는 한 번만 하도록 신경 써야 한다. (중복 해제를 방지해야 한다.)
  6. 포인터가 가리키는 객체가 여전히 살아있는지 알 방법이 없다.


스마트 포인터(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_ptrstd::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_ptrstd::shared_ptr로 변환이 쉽고 효율적이다.

따라서 std::unique_ptr로 반환할 경우 클라이언트에서는 상황에 맞게 유연하게 구현할 수 있다.


카테고리:

업데이트: