본문 바로가기
C++/Effective Modern C++

new 보다 std::make_shared와 make_unique를 선호하라

by COCO1337 2020. 7. 15.

std::make_shared는 C++11의 일부지만 std::make_unique는 C++14에 와서 표준 라이브러리에 포함되었다. 하지만 간단하게 구현할 수 있다.

template<typename T, typename... Ts>		// C++11 용으로 구현한 make_unique
std::unique_ptr<T> make_unique(Ts&&... params)
{
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

new 보다 make 함수를 선호해야하는 첫번째 이유로는 코드의 중복을 없애는데 있다.

auto upw1(std::make_unique<Widget>());		// make 함수를 사용
std::unique_ptr<Widget> upw2(new Widget);	// 사용하지 않음

되풀이되는 Widget의 중복을 없앨 수 있다.

 

두번째 이유로는 예외 안전성과 관련이 있다.

// Widget 객체를 그 객체의 우선순위에 따라 적절히 처리하는 함수
void processWidget(std::shared_ptr<Widget> spw, int priority);	// 내부에서 항상 std::shared_ptr 복사본 생성

int computePriority();	// 우선 순위 계산하는 함수

processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

이렇게 짜여진 경우 마지막 문장에서 자원 누수가 있을 수 있다. 컴파일러가 원시 코드를 목적 코드로 번역할 때 문제가 발생하게 되는데 실행 시점에서 함수가 호출 될 때 인수들의 평가가 우선적으로 이루어진다. 하지만 computePriority()의 경우 인수들에 대한 평가와 std::shared_ptr 생성자 사이에 호출되게 되면 동적으로 할당 된 Widget 객체가 새게 된다.

processWidget(std::make_shared<Widget>(), computePriority());	// 자원 누수 위험 없음

하지만 위 문장처럼 make 함수를 사용한 경우 만약 compuitePriority()가 먼저 실행되어 예외를 발생시켜도 자원 누수의 위험이 없다.

std::shared_ptr<Widget> spw(new Widget);
auto spw = std::make_shared<Widget>();

또한 new를 사용하면 Widget에대한 할당, std::shared_ptr에 대한 할당 총 2번의 할당이 있지만 auto를 사용한 2번째 문장에서는 한번의 할당으로 충분하다.


std::unique_ptr와 std::shared_ptr는 커스텀 삭제자를 받는 생성자를 제공하는데 반해 make 함수들은 커스텀 삭제자들을 지정할 수 없다.

auto widgetDeleter = [](Widget* pw) { ... };
std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
// make 함수들은 불가능하다

auto upv = std::make_unique<std::vector<int>>(10, 20);
// 모든 값이 20인 요소 10개 짜리 std::vector<int>를 생성한다.

여기서 볼 때, make 함수는 매배견수들을 완벽 전달할 때 중괄호가 아니라 괄호를 사용함을 뜻한다. 하지만 피지칭 객체를 중괄호 초기치로 생성하려면 반드시 new를 직접 사용해야 한다는 것이다. 하지만 여기에는 std::initializer_list를 사용한 우회책이 있다.

// std::initializer_list 객체 생성
auto initList = {10, 20};
// std::initializer_list 객체 이용해서 std::vector 생성
auto spv = std::make_shared<std::vector<int>>(initList);

클래스 고유의 operator new 와 operator delete를 정의하는 것들이 있다. 어떤 형식에 이런 함수가 존재한다는 것은 전역 메모리 할당 루틴과 해제 루틴이 그 형식의 객체에 맞지 않는다는 것을 뜻한다. 보통 클래스를 위한 operator new와 operator delete는 크기가 정확히 sizeof(클래스) 인 메모리 조각들의 할당과 해제를 처리하는데 특화 된 경우가 많다. 하지만 커스텀 std::shared_ptr의 커스텀 할당과 커스텀 해제에는 동적으로 할당된 크기는 객체의 크기에 제어 블록을 합한 크기이다. 그렇기 때문에 커스텀 std::shared_ptr의 커스텀 할당과 커스텀 해제에는 make함수로 생성하는 것은 바람직하지 않다.

new의 직접 사용에 비한 std::make_shared의 크기 및 속도상의 장점은 std::shared_ptr의 제어 블록이 관리 대상 객체와 동일한 메모리 조각에 놓인다는 점에서 비롯된다. 객체의 참조 횟수가 0이 되면 객체가 파괴된다. 하지만 객체가 차지하고 있던 메모리는 해당 제어블록이 파괴되기 전까지는 해제될 수 없다. 객체와 제어 블록이 동적으로 할당된 같은 메모리 조각에 있기 때문이다. 즉, std::shared_ptr용 make 함수가 할당한 메모리 조각은 그것을 참조하는 마지막 std::shared_ptr와 마지막 std::weak_ptr가 모두 파괴된 후에 해제할 수 있다. 객체가 크고 마지막 std::shared_ptr의 파괴와 마지막 std::weak_ptr의 파괴 시간 간격이 넓다면 객체가 파괴된 시점과 메모리 해제 시점 사이에 지연이 생길수 있다.

하지만 new 를 사용했을 경우에 마지막 std::shared_ptr가 파괴되는 즉시 그 객체의 메모리가 해제될 수 있다.

class ReallyBigType { ... };
auto pBigObj = std::make_shared<ReallyBigType>();	// make 함수 사용해서 생성
...	// 객체를 가리키는 마지막 std::shared_ptr가 파괴되지만 std::weak_ptr은 남아있다
... 	// 메모리는 여전히 할당된 상태, 마지막 std::weak_ptr이 파괴되어야 제어 블록과 객체를 차지하던 메모리 해제

std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);	// new를 이용해 생성
...	// 객체를 가리키는 마지막 std::shared_ptr가 파괴되며 객체의 메모리는 해제, std::weak_ptr은 남아있다
... 	// 마지막 std::weak_ptr이 파괴되면 제어 블록의 메모리도 해제

make 함수를 사용하지 못하는 상황에서 new를 사용하며 예외 안전성 문제를 겪지 않는 최선의 방책은 다른 일은 전혀 하지 않는 문장에서 new를 사용하고, 그 문장에서 smart pointer로 즉시 넘겨주는 것이다.

예외 안전성 부분의 첫번째 예제를 고쳐보자

void cusDel(Widget *ptr);		// 커스텀 삭제자

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

이런 식으로 수정하면 생성자에서 예외가 발생해도 std::shared_ptr는 생성자로 전달된 raw pointer의 소유권을 확보하기 때문에 "new Widget"으로 생성된 포인터에 대해 cusDel이 호출되어 메모리 누수가 생기지 않는다. 여기서 비효율성 문제를 해결하기 위해 예외에 안전하지 않은 호출에서는 processWidget에 오른값을 넘겨주고, 예외에 안전한 호출에서는 왼값을 넘겨준다

// 인수가 오른값
processWidget(std::shared_ptr<Widget>(new Widget, cusDel), computePriority());

// 인수가 왼값
processWidget(spw, computePriority());

Conclusion

- new 의 직접 사용에 비해, make 함수를 사용하면 소스 코드의 중복이 없어지고 예외 안전성이 확보되며, std::make_shared의 경우 더 작고 빠른 코드가 산출된다.

- make 함수의 사용이 불가능 또는 부적합한 경우로는 커스텀 삭제자를 지정 해야 하는 경우와 중괄호 초기치를 전달해야 하는 경우가 있다.

- std::shared_ptr에 대해서는 make 함수가 부적합한 경우가 있는데, 첫째로 커스텀 메모리 관리를 가진 클래스를 다루는 경우와 둘째, 메모리가 넉넉하지 않은 시스템에서 큰 객체를 다루고, std::weak_ptr들이 해당 std::shared_ptr보다 오래 살아남는 경우이다.


Reference

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

반응형

댓글