본문 바로가기

컴퓨터과학

[Effective C++] (18-3) 소유권 독점 자원의 관리에는 std::unique_ptr를 사용할 것

C++ unique_ptr

- 생 포인터(이를테면 new로 얻은 포인터) std::unique_ptr에 배정하는 문장은 컴파일되지 않습니다.  그런 문장을 허용한다면, 생 포인터에서 똑똑한 포인터로의 암묵적 변환이 성립하기 때문입니다. 그런 암묵적 변환에는 문제가 있으므로, C++11의 똑똑한 포인터들은 그런 변환을 금지합니다. 이 때문에, new로 생성한 객체의 소유권을 pInv에 부여하기 위해 reset을 호출했습니다.

 

- new 호출에서는 makeInvestment 함수에 전달된 인수들을 new에 완벽하게 전달하기 위해 std::forward를 사용했습니다(Chapter(25) 참고). 이렇게 하면 호출자가 함수에 제공한 모든 정보를 함수 안에서 생성할 객체의 생성자에게 손실 없이 넘겨줄 수 있습니다.

 

- 커스텀 삭제자는 Investment* 형식의 매개변수를 받습니다. makeInvestment 안에서 생성하는 객체의 실제 형식(Stock 이나 Bond, RealEstate)이 무엇이든, 그 객체는 람다 표현식 안에서 Investment* 객체로서 delete 됩니다. , 기반 클래스 포인터를 통해서 파생 클래스의 객체를 삭제하는 것입니다. 이것이 제대로 작동하려면 기반 클래스, Investment의 소멸자가 가상 소멸자이어야 합니다.

 

class Investment {

public:

 ...

 virtual ~Investment(); // 필수적인 설계 요소

 ...

};

 

C++14는 함수 반환 형식의 연역을 지원하므로(Chapter(3) 참고), makeInvestment를 다음과 같이 좀 더 간결하고 캡슐화된 방식으로 구현할 수 있습니다.

 

template<typename... Ts>

auto makeInvestment(Ts&&... params) // C++14

{

 auto delInvmt = [](Investment* pInvestment) // 이제는 make-Investment의 내부에서 삭제자를 정의

 {

  makeLogEntry(pInvestment);

  delete pInvestment;

 };

 

 std::unique_ptr<Invesetment, decltype(delInvmt)> // 이전과 동일

 pInv(nullptr, delInvmt);

 

 if ( ... )

 {

  pInv.reset(new Stock(std::forward<Ts>(params)...));

 }

 else if ( ... )

 {

  pInv.reset(new Bond(std::forward<Ts>(params)...));

 }

 else if ( ... )

 {

  pInv.reset(new RealEstatae(std::forward<Ts>(params)...));

 }

 return pInv;

}

 

앞에서 언급했듯이, 기본 삭제자( delete)를 사용할 때에는 std::unique_ptr 객체의 크기가 생 포인터의 크기와 같으리라고 가정하는 것이 합당합니다. 그러나 커스텀 삭제자를 사용하면 상황이 달라집니다.일반적으로, 함수 포인터를 삭제자로 지정한 경우에는 std::unique_ptr의 크기가 1 워드에서 2 워드로 증가합니다. 삭제자가 함수 객체일 때에는 std::unique_ptr의 크기가 그 함수 객체에 저장된 상태의 크기만큼 증가합니다. 상태 없는 함수 객체(이를테면 가무리 없는 람다 표현식이 산출한)의 경우에는 크기 변화가 없으며, 따라서 삭제자를 보통의 함수로 구현할 수도 있고 갈무리 없는 람다 표현식으로 구현할 수 있는 경우라면 람다 쪽을 선호하는 것이 바람직합니다.

 

auto delInvmt1 = [](Investment* pInvestment) // 상태 없는 람다 형태의 삭제자

{

 makeLogEntry(pInvestment);

 delete pInvestment;

};

 

template<typename... Ts> // 반환 형식은 Investment*와 같은 크기

std::unique_ptr<Investment, decltype(delInvmt1)>

makeInvestment(Ts&&... args);

 

void delInvmt2(Investment* pInvestment) // 함수 형태의 삭제자

{

 makeLogEntry(pInvestment);

 delete pInvestment;

}

 

template<typename... Ts> // 반환 형식의 크기는 Investment*의 크기에 적어도 함수 포인터의 크기를 더한 것

std::unique_ptr<Investment, void (*)(Investment*)>

makeInvestment(Ts&&... params);

 

상태가 많은 함수 객체 삭제자를 사용한다면 std::unique_ptr 객체의 크기가 상당히 커질 수 있습니다. 커스텀 삭제자 때문에 std::unique_ptr가 허용 가능한 수준 이상으로 커진다면, 설계 자체를 변경해야 할 수 있습니다.

그런데 std::unique_ptr의 흔한 용도가 팩터리 함수만은 아닙니다. std::unique_ptr Pimpl 관용구의 구현 메커니즘으로 더 인기 있습니다. 이를 위한 코드가 아주 복잡하지는 않지만 아주 간단하다고 할 수도 없기 때문에, 관련 예제는 그 관용구를 중점적으로 다루는 Chapter(22)로 미루기도 합니다.

std::unique_ptr는 두 가지 형태인데, 하나는 개별 객체를 위한 것(std::unique_ptr<T>)이고 또 하나는 배열을 위한 것(std::unique_ptr<T[]>)입니다. 이처럼 두 가지 형태가 있으므로, std::unique_ptr가 어떤 종류의 개체를 가리키는지 관련된 애매함이 절대 발생하지 않습니다. std::unique_ptr API는 사용 대상에 잘 맞는 형태로 설계되어 있습니다. 예를 들어 개별 객체 버전은 색인 적용 연산자(operator[])를 제공하지 않으며, 배열 버전은 역참조 연산자들(operator* operator->)을 제공하지 않습니다.

배열용 std::unique_ptr가 있다는 사실은 그냥 지적인 흥미 거리 정도로만 받아들이기 바랍니다. 내장 배열보다는 std::array std::vector, std::string이 거의 항상 더 나은 선택이기 때문입니다. 힙에 생성된 배열을 가리키는 생 포인터를 돌려주는(그리고 그 배열을 클라이언트가 소유하는) C 스타일 API를 다루어야 하는 경우를 제외하면, std::unique_ptr<T[]>를 사용하는 것이 합당한 상황을 생각해내기 힘듭니다.

C++11에서 독점 소유권을 표현하는 주된 방법이라는 점 외에, std::unique_ptr std::shard_ptr로의 변환이 쉽고도 효율적이라는 아주 매력적인 특징도 가지고 있습니다.

 

std::shared_ptr<Investment> sp = std::unique_ptr std::shared_ptr로 변환

 makeInvestment( 인수들 );

 

이는 std::unique_ptr가 팩터리 함수의 반환 형식으로 아주 적합한 이유의 핵심적인 한 부분입니다. 팩터리 함수는 자신이 돌려준 객체를 호출자가 독점적으로 소유하려 하는지, 아니면 소유권을 공유하고자 하는지(std::shared_ptr에 해당)미리 알 수 없습니다. 팩터리 함수가 std::unique_ptr를 반환한다면 호출자는 가장 효율적인 똑똑한 포인터를 얻게 되며, 게다가 그것을 좀 더 유연한 동기(sibling)로 변환할 수 있는 여지도 생깁니다. (std::shared_ptr에 관해서는 다음 항목, Chapter(19)를 참고.)

 

 

*** Key Point ***

- std::unique_ptr는 독점 소유권 의미론을 가진 자원의 관리를 위한, 작고 빠른 이동 전용 똑똑한 포인터입니다.

- 기본적으로 자원 파괴는 delete를 통해 일어나나, 커스텀 삭제자를 지정할 수도 있습니다. 상태 있는 삭제자나 함수 포인터를 사용하면 std::unique_ptr 객체의 크기가 커집니다.

- std::unique_ptr std::shared_ptr로 손쉽게 변환할 수 있습니다.