본문 바로가기

컴퓨터과학

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

C++ ptr

예외나 기타 비전형적 제어 흐름(이를테면 때 이른 함수 반환이나 루프로부터의 break )때문에 그러한 일련의 소유권 이전이 중도에 가로 차이면,결국에는 관리되는 자원을 소유하고 있던 std::unique_ptr의 소멸자가 호출되며, 결과적으로 관리되는 자원이 파괴됩니다.

 

(* 이 규칙에는 몇 가지 예외가 있습니다. 그 예외들은 대부분 비정상적인 프로그램 종료에서 비롯됩니다. 예외가 스레드의 주 함수(예를 들어 프로그램의 초기 스레더의 경우에는 main) 바깥으로까지 전파되거나 noexcept 명세가 위반되면(Chapter(14) 참고) 지역 객체들이 파괴되지 않을 수 있으며, std::abort나 어떤 종료 함수( std::Exit std::exit, std::quick_exit)가 호출되면 지역 객체들은 절대로 파괴되지 않습니다.)

 

그러한 파괴는 기본적으로 delete를 통해서 일어나지만, std::unique_ptr 객체를 생성할 때 커스텀 삭제자(custom deleter)를 사용하도록 지정하는 것도 가능합니다. 커스텀 삭제자는 해당 자원의 파괴 시점에서 호출되는 임의의 함수(또는, 람다 표현식으로부터 산출되는 것들을 포함한 함수 객체)입니다. makeInvestment가 생성한 객체를 직접 delete로 파괴할 수 없고 먼저 로그 항목을 기록한 후에 파괴해야 한다면, makeInvestment를 다음과 같이 구현하면 될 것입니다. (코드 다음에 설명이 나오니, 코드만 보고 그 의도가 명확하게 이해되지 않는 부분이 있어도 걱정하지 말기 바랍니다.)

 

auto delInvmt = [](Investment* pInvestment) // 커스텀 삭제자(람다 표현식)

{

 makeLogEntry(pInvestment);

 delete pInvestment;

};

 

tmeplate<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를 사용한다는 것은, 프로그램의 모든 경로에서 파괴가 정확히 한 번만 일어남이 보장됨을 뜻할 뿐만 아니라, 자원의 파괴 방식에 대해 신경 쓸 필요가 전혀 없음을 뜻하기 때문입니다. 그 모든 것을 std::unique_ptr가 자동으로 처리해줍니다. 클라이언트의 관점에서 makeInvestment의 인터페이스는 그야말로 유쾌합니다.

다음 사항들을 이해한다면, 구현 역시 상당히 멋지다는 점을 깨닫게 될 것입니다.

 

- delInvmt makeInvestment가 돌려준 객체에 대한 커스텀 삭제자입니다. 모든 커스텀 삭제 함수는 파괴하라 객체를 가리키는 포인터 하나를 받으며, 그 객체를 파괴하는 데 필요한 일들을 수행합니다. 지금 예에서 삭제자가 해야 할 일은 makeLogEntry를 호출한 후 delete를 적용하는 것입니다. 람다 표현식을 이용해서 delInvmt를 생성하는 것은 편리할 뿐만 아니라, 잠시 후에 보겠지만 통상적인 함수를 작성하는 것보다 더 효율적이기도 합니다.

 

- 커스텀 삭제를 사용할 때에는 그 형식을 std::unique_ptr의 둘째 형식 인수로 지정해야 합니다. 지금 예에서 삭제자의 형식은 delInvmt의 형식이며, makeInvestment의 반환 형식이std::unique_ptr <Investment, decltype(delInvmt)>인 것은 바로 그 때문입니다. (decltype에 관해서는 Chpater(3)을 참고.)

 

- makeInvestment의 기본 전략은 널 std::unique_ptr를 만들어서 적절한 형식의 객체를 가리키게 한 후 돌려주는 것입니다. 커스텀 삭제자 delInvmnt pInv에 연관시키기 위해, pInv 생성 시 delInvmt를 둘째 형식으로 지정합니다.