스토리텔링 개발자

[Effective Modern C++] 18. std::unique_ptr 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 18. std::unique_ptr

김디트 2025. 2. 28. 11:33
728x90

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

 

 

 

std::unique_ptr
  • raw 포인터와 같은 크기라고 가정할 수 있다.
    • 대부분의 연산에서 raw 포인터와 동일한 명령들을 실행한다.
  • 독점적 소유권(exclusive ownership) 의미론을 토대로 구현되어 있다.
    • 널이 아닌 std::unique_ptr는 항상 자신이 가리키는 객체를 소유하고 있다.
    • 이동하면 소유권은 옮겨지고, 기존 포인터는 널로 설정된다.
    • 소유권 문제로 복사는 허용되지 않는다.
    • 즉, 이동 전용 타입(move-only type)이다.
  • 두 가지 타입이 존재한다.
    • 개별 객체를 위한 타입(std::unique_ptr<T>)
      • operator[] 를 제공하지 않는다.
    • 배열을 위한 타입(std::unique_ptr<T[]>)
      • operator*와 operator-> 를 제공하지 않는다.
  • std::shared_ptr로의 변환이 쉽고 효율적이다.
    • 그렇기 때문에 팩토리 함수의 리턴 타입으로 unique_ptr은 아주 적합하다.
std::unique_ptr<Investment> up;
...
std::shared_ptr<Investment> sp = up; // 할당만으로 변환이 가능하다.

 

 

 

std::unique_ptr 예제
  • 계통 구조(hierarchy) 안의 객체를 생성하는 팩토리 함수 리턴 타입으로 사용하기.

class Investment { ... };

class Stock : public Investment { ... };

class Bond : public Investment { ... };

class RealEstate : public Investment { ... };

template<typename... Ts>
std::unique_ptr<Investment> makeInvestment(Ts&&... params);

// 사용 예
{
    ...
    auto pInvestment = makeInvestment( 인수들 );
    ...
} // *pInvestment가 파괴된다.
  • 커스텀 삭제자(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;
}

 

 

 

톺아보기
  • 커스텀 삭제자로 람다식을 사용하면 효율적이다.
    • 아래의 커스텀 삭제자와 std::unique_ptr 객체의 크기에서 자세히 설명 
  • 커스텀 삭제자를 사용할 때엔 그 타입을 std::unique_ptr의 둘째 타입 인수로 지정해야 한다.
  • 커스텀 삭제자에선 Investment* 객체를 delete 하는데, 정상적으로 상속되려면 하위 클래스들의 소멸자가 가상 소멸자여야 한다.
  • raw 포인터를 std::unique_ptr에 할당하는 문장은 컴파일되지 않는다.
    • 만약 허용된다면 raw 포인터에서 스마트 포인터로의 암시적 변환이 허용된다는 뜻이기에..
  • 전달된 인수들을 new로 손실없이 전달하기 위해 std::forward를 사용했다.(항목 25 참조)

 

 

 

커스텀 삭제자와 std::unique_ptr 객체의 크기
  • 커스텀 삭제자를 사용하면 raw 포인터와 std::unique_ptr 객체의 크기가 같다는 가정이 깨진다.
    • 일반적으로 함수 포인터를 삭제자로 지정하면 1워드에서 2워드로 크기가 증가한다.
    • 함수 객체를 삭제자로 지정하면 함수 객체에 저장된 상태의 크기만큼 증가한다.
    • 상태 없는 함수 객체(혹은 캡쳐 없는 람다 표현식)의 경우 크기 변화가 없다.
// 캡쳐 없는 람다 타입 삭제자
auto delInvmt1 = [](Investment* pInvestment)
                 {
                     makeLogEntry(pInvestment);
                     delete pInvestment;
                 };
                 
// 리턴되는 std::unique_ptr은 Investment*와 크기가 같다.
template<typename... Ts>
std::unique_ptr<Investment, deltype(delInvmt1)> makeInvestment(Ts&&... args);



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

// 리턴되는 std::unique_ptr은 최소 Investment* + 함수 포인터의 크기
template<typename... Ts>
std::unique_ptr<Investment, void (*)(Investment*))> makeInvestment(Ts&&... args);

 

728x90
Comments