본문 바로가기
C++/Effective Modern C++

std::unique_ptr

by COCO1337 2020. 7. 12.

std::unique_ptr는 항상 자신이 가리키는 객체를 소유한다. std::unique_ptr를 이동시 소유권이 원본 포인터에서 대상 포인터로 옮겨진다. std::unique_ptr의 복사는 허용되지 않는다. 만약 복사가 가능하다면 두개의 std::unique_ptr가 같은 자원을 가리키며 두 포인터 모두 그 자원을 소유하고있다는 얘기가 된다. 그렇기 때문에 std::unique_ptr은 이동 전용(move-only) 형식이며 소멸시 자신이 가리키는 자원을 파괴한다. 기본적으로 파괴는 std::unique_ptr안에 있는 생포인터에 delete를 적용함으로써 수행된다.

 

std::unique_ptr은 Hierachy 구조에서 객체를 생성하는 팩터리 함수의 반환형식으로 쓰기에 적합하다. hierachy 구조에서 팩터리 함수는 흔히 힙에 객체를 생성하고 그 객체를 가리키는 포인터를 돌려준다. 그 객체가 더 이상 필요하지 않게 되엇을 때 객체를 삭제하는 것은 호출자의 몫이다. std::unique_ptr은 소멸시 피지칭 객체를 자동으로 삭제해 주기 때문에 hierachy 구조에 적합하다.

파괴는 기본적으로 delete를 통해서 일어나지만, std::unique_ptr 객체를 생성할 때 커스텀 삭제자를 지정하는 것도 가능하다. 다음 예시는 투자 대상을 대표하는 Investment 를 base class로 삼은 hierachy 구조이다.

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;
}

 호출자 관점에서 makeInvestment 호출 결과를 하나의 auto 변수에 저장한다고 할 때, 삭제에 특별한 필요가 없다. std::unique_ptr은 모든 경로에서 파괴가 정확히 한 번만 일어나며 자원 파괴 방식에 신경 쓸 필요 없이 자동으로 처리 해준다.

- delInvmt는 makeInvestment가 돌려준 객체에 대한 커스텀 삭제자이다. 예시에서 삭제자가 하는일은 makeLogEntry를 호출한 후 delete를 적용

- 커스텀 삭제자를 사용할 때에는 std::unique_ptr의 둘째 형식 인수로 지정해야 한다.  예시에서 삭제자의 형식은 delInvmt형식이다.

- makeInvestment의 기본 전략은 null std::unique_ptr을 만들어 적절한 형식의 객체를 가리키게 한 후 돌려주는 것이다.

- raw pointer를 std::unique_ptr 에 배정하는 문장은 컴파일되지 않는다. 만약 이게 성립한다면 raw pointer에서 smart pointer로의 암묵적 변환이 성립하게 된다. 때문에 new로 생성한 객체의 소유권을 reset으로 호출해서 pInv에 부여했다.

- 각 new 호출에서 params를 전달하기 위해 std::forward를 사용했다.

- 커스텀 삭제자는 Investment* 형식의 매개변수를 받는다. makeInvestment 안에서 생성하는 객체 실제 형식이 무엇이든 람다 표현식 안에서 Investment* 객체로서 delete 된다. 즉 base class pointer를 통해서 파생 클래스의 객체를 소멸하는 것이기 때문에 Investment의 소멸자가 가상소멸자여야 한다.

class Investment{
public:
  ...
  virtual ~Investment();
  ...
};

기본 삭제자를 사용할 때에는 std::unique_ptr 객체의 크기가 raw pointer 크기와 같다. 하지만 커스텀 삭제자를 사용할 경우 std::unique_ptr의 크기가 1워드에서 2워드로 증가하며, 함수 객체일 경우 함수 객체의 크기만큼 증가한다. 상태 없는 함수 객체의 경우 크기 변화가 없으며, 삭제자를 보통 함수로 구현할 수도 있고 갈무리 없는 람다 표현식으로 구현할 수 있는 경우라면 람다식을 선호하는 것이 좋다.

auto delInvmt1 = [](Investment* pInveestment)	// 상태 없는 람다 형태의 삭제자
{
  makeLogEntry(pInvestment);
  delete pInvestment;
};

template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt1)>	// 반환 형식은 Investment*와 같은 크기
makeInvestment(Ts&&... args);

void delInvmt2(Investment* pInvestment)		// 함수 형태의 삭제자
{
  makeLogEntry(pInvestment);
  delete pInvestment;
};

template<typename... Ts>	
std::unique_ptr<Investment, void (*)(Investment*)>	// 반환 형식의 크기는 Investment* 크기 + 함수 함수포인터의 크기
makeInvestment(Ts&&... params);

std::unique_ptr은 두가지 형태가 있다. 개별 객체를 위한 것과 배열을 위한 것인데, 두 형태가 구분되어 있기 때문에 애매함이 발생하지 않는다. 하지만 내장 배열보다는 std::array나 std::vector, std::string이 거의 항상 나은 선택이기 때문에 배열을 위한 것은 생각하지 않는 것이 좋다.

 

또한 std::unique_ptr는 std::shared_ptr로 변환이 쉽고 효율적이다.

std::shared_ptr<Investment> sp = makeInvestment(인수들);

 


Conclusion

- std::unique_ptr는 독점 소유권 의미론을 가진 자원 관리를 위한 작고 빠른 이동 전용 smart pointer이다

- 기본적인 자원 파괴는 delete를 통해 일어나지만 커스텀 삭제자를 지정할 수 있고 이 경우 std::unique_ptr객체의 크기가 커질 수 있다.

- std::unique_ptr를 std::shared_ptr로 손쉽게 변환이 가능하다


Reference

Effective Modern C++ 항목 18: 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라

반응형

댓글