스토리텔링 개발자

[Effective Modern C++] 21. std::make_unique, std::make_shared 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 21. std::make_unique, std::make_shared

김디트 2025. 3. 6. 11:23
728x90

항목 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라

 

 

 

특징
  • std::make_shared는 C++11에서 표준에 포함되었다.
  • std::make_unique는 C++14에서 표준에 포함되었다.
  • 허나 make_unique를 만드는 건 어렵지 않다.
// 배열 버전은 지원하지 않지만, 쉽게 지원하도록 추가할 수 있을 것이다.
// std namespace에 넣진 말자. 14가 되면 충돌할 것이다.
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
  • 스마트 포인터를 리턴하는 make 함수는 한 가지 더 있다.
  • std::allocate_shared
    • std::make_shared처럼 동작하지만, 첫 인수가 동적 메모리 할당에 쓰일 할당자 객체이다.

 

 

 

make 함수를 사용하여 스마트 포인터를 생성해야 하는 이유
  • 타입을 여러 번 타이핑 해야 해서 유지보수가 귀찮다.
auto upw1(std::make_unique<Widget>()); // 'Widget'을 한번만 타이핑한다.
std::unique_ptr<Widget> upw2(new Widget); // 'Widget'을 여러 번 타이핑한다.

auto spw1(std::make_shared<Widget>()); // 동일
std::shared_ptr<Widget> spw2(new Widget); // 동일
  • 예외 안전성이 떨어진다.
    • make 함수를 사용하면 순서가 섞여 자원 누수가 발생할 여지가 차단된다.
void processWidget(std::shared_ptr<Widget> spw, int priority);

int computePriority();

// 자원 누수의 위험이 있다!
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

// 1. new Widget으로 힙에 공간 할당
// 2. std::shared_ptr<Widget>의 생성자가 실행된다.
// 3. computePriority가 실행된다.

// 허나 이 순서가 절대적인 것은 아니다.
// 아래처럼 진행될 수도 있다.

// 1. new Widget으로 힙에 공간 할당
// 2. computePriority가 실행된다.
// 3. std::shared_ptr<Widget>의 생성자가 실행된다.

// 이 경우 computePriority에서 예외가 던져지면 누수!
  • 컴파일러가 생산하는 코드의 효율성이 차이난다.
std::shared_ptr<Widget> spw(new Widget); // 한 번의 할당?
// 실제로는 두 번의 할당이 발생한다.
// 제어 블록 메모리가 할당되기 때문이다.

auto spw = std::make_shared<Widget>(); // 한 번의 할당만 발생한다.
// std::make_shared가 Widget 객체와 제어블록 모두를 담을 메모리 조각을 한번에 할당한다.

 

 

 

make 함수를 사용하면 안 되는 경우(공용)
  • 커스텀 삭제자를 지정해야 하는 경우(항목 18, 항목 19 참조)
    • make 함수들에는 커스텀 삭제자를 지정할 수 있는 버전이 없다.
    • 하지만 스마트 포인터들은 커스텀 삭제자를 받는 생성자를 제공한다.
auto widgetDeleter = [](Widget* pw) { ... };

// 커스텀 삭제자를 사용한 스마트 포인터들
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
  • 생성자로 std::initializer_list를 넘겨야 하는 경우
    • 원래라면 중괄호를 사용하면 std::initializer_list 버전이 선택된다.
    • make 함수들에는 어떻게 사용해야 할까?
    • // 값이 20인 요소 10개?
      // 요소 10, 20?
      auto upv = std::make_unique<std::vector<int>>(10, 20);
      auto spv = std::make_shared<std::vector<int>>(10, 20);
      
      // 정답은 값이 20인 요소 10개이다.
      // make 함수들은 내부적으로 괄호를 사용한다는 뜻이다.
      // 즉, 중괄호 초기치를 사용하려면 new를 사용해야 한다는 뜻이다.
    • 중괄호 초기치는 완벽 전달이 불가능하기 때문이다.(항목 30 참조)
    • 허나 아래와 같이 하면 우회 해결이 가능하다.
    • // std::initializer_list 객체 생성
      auto initList = { 10, 20 };
      
      // 해당 객체를 전달
      auto spv = std::make_shared<std::vector<int>>(initList);

 

 

 

make 함수를 사용하면 안 되는 경우(shared_ptr)
  • 메모리 할당을 커스텀한 경우
    • 이 경우 allocate_shared와 잘 맞지 않는다.
    • allocate_shared가 요구하는 메모리는 단순하게 '객체 크기 + 제어 블록 크기'이기 때문이다.
  • 객체 파괴 시점과 객체의 메모리 해제 시점 사이의 시간 지연
    • make_shared는 객체와 제어 블록을 한 메모리 공간에 할당하기 때문에(코드 효율성)
    • 제어 블록이 파괴되기 전까지는 객체 메모리까지 할당 해제할 수 없다는 문제가 있다.
    • 제어 블록은 shared_ptr과 그를 기준으로 만들어진 weak_ptr이 존재하는 한 계속 존재한다.
    • 그러므로 객체 파괴 시점과 객체 점유 메모리 해제 시점 사이엔 시간 지연이 발생할 수 있다.
    • weak_ptr이 남아있는 한 해제가 되지 않기 때문이다.
    • class ReallyBigType { ...}
      
      auto pBigObj = std::make_shared<ReallyBigType>(); // 아주 큰 객체 할당
      
      ... // shared_ptr과 weak_ptr 할당
      
      ... // shared_ptr 모두 파괴. 허나 weak_ptr은 잔존.
      
      ... // ReallyBigType 객체 메모리는 여전히 할당된 상태이다.
      
      ... // weak_ptr 모두 파괴. 메모리 해제.
       
    • 같은 코드라도 new를 사용하면 shared_ptr 해제 시 즉시 해제가 된다.
      • 메모리 블록이 공유되지 않기 때문이다.

 

 

 

new를 사용한 스마트 포인터 생성 시 주의점
  • new의 결과를 다른 일은 전혀 하지 않는 문장에서 스마트 포인터의 생성자에 즉시 넘겨준다.
// 자원 누수의 위험이 있는 코드
processWidget(std::shared_ptr<Widget>(new Widget, cusDel), computePriority());

// 개선
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());

// 하지만 기존엔 rvalue로 넘기던 것이 lvalue가 되면서 복사가 되는 문제.

// 개선 2
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority());
728x90
Comments