- 생 포인터(이를테면 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로 손쉽게 변환할 수 있습니다.
'컴퓨터과학' 카테고리의 다른 글
C++ (19-2) 소유권 공유 자원의 관리에는 std::shared_ptr를 사용할 것 (0) | 2020.06.20 |
---|---|
C++ (19-1) 소유권 공유 자원의 관리에는 std::shared_ptr를 사용할 것 (0) | 2020.06.19 |
[Effective C++] (18-2) 소유권 독점 자원의 관리에는 std::unique_ptr를 사용할 것 (0) | 2020.06.15 |
[Effective C++] (똑똑한 포인터) / Chapter(18-1) 소유권 독점 자원의 관리에는 std::unique_ptr를 사용할 것 (0) | 2020.06.14 |
[Effective C++] (17-4) 특수 멤버 함수들의 자동 작성 조건을 숙지할 것 / (똑똑한 포인터) (0) | 2020.06.13 |