make_shared는 C++11의 표준이지만 make_unique는 C++14의 표준이다.

allocate_shared라는 함수도 있는데 이건 커스텀 할당자를 포함한 shared_ptr를 만들어준다.

 

각설하고 왜 make 함수를 선호하라고 하는건지 알아보도록 하자.

 

auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);

auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

둘 다 스마트 포인터를 생성하는 문법이다. 하지만 make 함수로 생성하는 경우 auto를 사용하기 때문에 타입을 중복해서 작성하지 않아도 된다.

 

또한 예외 안전성과 관련이 있다.

 

void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority(); // 우선순위를 반환하는 함수
...

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

위와 같은 코드에서 새로운 Widget 객체가 동적 할당되고 shared_ptr의 생성자가 호출된 후 computePriority 함수가 호출되기를 기대한다. 하지만 반드시 그런것은 아니다. new Widget은 shared_ptr의 생성자보다 먼저 평가되는 것은 확실하지만 computePriority는 그보다 먼저 호출될수도, 아닐수도 있는 것이다.

심지어 중간에 호출될수도 있다. 그런 경우에 computePriority가 예외를 던졌다면 new Widget에 의해 객체가 동적할당 되었지만 shared_ptr의 생성자를 호출하지 못해서 자원이 새는 경우가 발생하게 되는 것이다.

 

processWidget(std::make_shared<Widget>(), computePriority());

하지만 make_shared로 shared_ptr를 생성한다면 둘의 호출 순서가 바뀔수는 있어도 중간에 호출되는 일은 발생하지 않기 때문에 근본적으로 자원 누수가 발생할 가능성이 끼어들지 못한다.

 

 

std::shared_ptr<Widget> spw(new Widget);

설령 아무런 예외가 발생하지 않는다고 해도 또다른 차이가 있다.

이 코드는 메모리 할당이 한번만 이루어질것 같지만 실제로는 두번 이루어진다. 동적 할당된 Widget 객체 말고도 제어 블록을 위한 또 다른 메모리 할당이 일어난다.

대신 make_shared를 사용하면 Widget 객체와 제어 블록 모두를 담을 수 있는 크기의 메모리 공간을 한번에 할당하기 때문에 프로그램의 정적 크기가 줄어들뿐만 아니라 메모리를 한번만 할당하므로 실행 속도 역시 빨라진다.

 

 

여기까지 언급했을때는 make 함수 사용이 모든 상황에서 좋아보이지만 아쉽게도 그렇지는 않다. 항상 사용하라는 것이 아니고 선호하라는 것이다.

그에 대한 몇가지 이유들이 존재한다.

 

첫째로 예전에도 언급했듯이 make함수는 커스텀 삭제자를 지정할 수 없다.

두번째로 중괄호 초기화를 사용하는 타입의 객체의 초기값을 완벽하게 전달할 수 없다. make 함수들은 내부적으로 매개변수들을 객체의 생성자를 완벽하게 전달하는데에 있어서 중괄호가 아닌 괄호를 사용한다. 만약 중괄호 초기화를 사용하는 객체의 초기값을 완벽하게 전달하고 싶다면 new를 사용하거나 초기화 리스트 객체를 미리 만들어서 make 함수에 전달해야한다.

 

여기까지가 unique_ptr의 make 함수 사용시의 문제점이고 안타깝게도 shared_ptr는 두가지가 더 있다.

 

만약 어떤 클래스가 기본으로 제공되는 new, delete가 적합하지 않아서 자신만의 메모리 관리를 위해 두 연산자를 오버로딩했다고 하자. 보통 이런 경우는 정확히 해당 객체 크기만큼의 메모리 조각들의 할당과 해제를 처리하는데 특화되어있다. 이런 것들은 shared_ptr의 커스텀 할당자와 커스텀 삭제자와 궁합이 잘 맞지 않는다.

왜냐면 커스텀 할당자가 요구하는 메모리 조각의 크기는 해당 객체의 크기가 아닌 제어 블록의 크기까지 더한것이기 때문이다. 그래서 클래스 고유의 new, delete 연산자가 있는 타입의 객체를 make 함수로 생성하는 것은 대체로 좋은 선택이 아니다.

 

 

weak_ptr이 객체의 만료 여부를 판단할 수 있는것은 제어 블록의 참조 횟수를 점검하기 때문이다. 그런데 참조 횟수가 0이 되었다고 객체와 더불어 제어 블록까지 소멸시켜 버리면 weak_ptr이 참조할 제어 블록이 사라지는 것이기 때문에 참조 횟수와 더불어 약참조 횟수까지 0이 되어야 제어 블록이 소멸된다.

make_shared를 통해 shared_ptr를 만든다면 메모리 할당을 한번만 한다고 했다. 이 뜻은 연속된 메모리 공간에 객체와 제어 블록을 배치한다는 것이고, 해제도 동시에 이루어져야 함을 의미한다. 하지만 weak_ptr가 존재하는 한, 참조 횟수가 0이 되어 객체를 사용할 수 없을지라도 메모리 공간은 계속 점유되기 때문에 마지막 shared_ptr의 파괴와 마지막 weak_ptr의 파괴 사이의 시간이 꽤 길다면 실제 객체가 파괴된 시점과 점유하던 메모리가 해제되는 시점 사이에 시간 지연이 생길 수 있다.

 

하지만 이런 경우에 make 함수 대신 new를 통해 메모리를 두번 할당하여 생성시킨다면 별개의 공간에 존재하기 때문에 마지막 shared_ptr가 소멸될 때, 객체 역시 소멸된다.

물론 예외 안전성의 문제를 무시할 수 없으므로 new를 통해 shared_ptr를 생성하는 경우에는 해당 문장에서는 오직 new의 결과를 shared_ptr의 생성자에 넘겨주는 작업만 해서 예외가 방출될 일이 존재하지 않도록 한다.

 

std::shared_ptr<Widget> spw(new Widget, cusDel); // 커스텀 삭제자
processWidget(spw, computePriority());

예외에 안전한 코드가 되었다. 하지만 한가지 아쉬운점이 있다면 spw가 왼값으로 넘어가기 때문에 복사 연산이 발생하게 된다. 실제로는 오른값으로 넘겨주던것을 예외에 안전한 코드를 작성하기 위해 왼값으로 만든것이기 때문에 이동 연산을 통해서 성능까지 챙겨주면 더 좋다.

 

std::shared_ptr<Widget> spw(new Widget, cusDel); // 커스텀 삭제자
processWidget(std::move(spw), computePriority());

그래도 make 함수를 사용하지 않고 shared_ptr를 생성시킬 일이 그렇게 많지는 않을 것이다.

 

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

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

◾ std::shared_ptr에 대해서는 make 함수가 부적합한 경우가 더 있는데, 두 가지 예를 들자면 (1) 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우와 (2) 메모리가 넉넉하지 않은 시스템에서 큰 객체를 자주 다루어야 하고 std::weak_ptr들이 해당 std::shared_ptr들보다 더 오래 살아남는 경우이다.

+ Recent posts