스마트포인터를 사용할 때 되도록이면 make_unique와 make_shared를 사용을 권장한다는 이야기를 많이 들어습니다. 오늘 그 이유에 대해서 글을 작성하려고 합니다. 이에 대한 이유를 파악하기 위해서는 CTAD라는 개념을 알아야 합니다.

 

템플릿 매개변수 추론 기능 Class Template Argument Deduction (CTAD)

C++17이후 부터 클래스 템플릿 생성자에 전달된 인수를 보고 템플릿 매개변수를 자동으로 추론하는 기능이 생겼습니다. 이를 Class Template Argument Dedction 줄여서 CTAD라고 합니다.

예시

// C++17 이전
std::pair<int, double> pairA = make_pair( 1, 5.3 );  // 직접 타입을 지정해주거나 
auto pairA = make_pair( 1, 5.3 );		 // auto의 도움을 받아야했었다.

// C++17이후 CTAD 기능
std::pair pairB( 1, 5.3 );  // 타입을 알아서 추론해준다.

위 코드에서 볼 수 있듯이 C++17 이전에는 템플릿 매개변수를 자동으로 추론해주지 않았기 때문에 프로그래머가 auto를 사용하거나 직접 타입을 명시해야 했습니다. 

하지만 pairB를 보면 알 수 있듯이, C++17 이후 부터는 특별히 템플릿 매개변수를 명시하지 않아도 자동으로 타입을 추론해서 생성자를 호출해주는 것을 확인할 수 있습니다. 이는 편리해보이지만 자칫하면 큰 문제가 발생할 수 있습니다.

만약 std::unique_ptr이나 shared_ptr의 생성자에 T*를 전달하면 컴파일러는 <T>와 <T[]>중에서 하나를 선택해야 하는데, 잘못 결정하면 치명적인 결과가 생길 우려가 있습니다. 그러므로 unique_ptr과 shared_ptr은 각각 make_unique()와 make_shared()를 사용하도록 작성하는 것이 권장되고 있습니다.

 

결론

  • 타입 안정성
    위에서 언급한 것처럼 std::unique_ptr<T>와 std::unique_ptr<T[]> 사이에서 잘못된 선택을 할 수 있습니다. 이는 동적 할당된 배열이나 단일 객체에 대한 올바른 삭제 동작을 보장하지 않을 수 있습니다.

  • 예외 안정성
    std::make_shared를 사용하면 메모리 할당과 객체 생성이 한 단계에서 이루어지므로, 중간에 예외가 발생하더라도 메모리 누수 위험이 없습니다.

  • 메모리 효율
    std::make_shared는 std::shared_ptr에 필요한 control block을 객체와 동일한 메모리 할당에서 생성합니다. 이로 인해 추가적인 메모리 할당이 필요하지 않게 됩니다.