728x90
반응형

std::make_shared는 C++11의 일부이지만, std::make_unique는 C++14에서 표준 라이브러리에 포함되었다.

하지만 C++11에서 make_unique와 같은 함수 템플릿을 만드는 것은 어렵지 않다.

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함수는 임의의 개수와 타입 인수들을 받아서 생성자로 전달한 후 객체를 동적으로 생성하고 그 객체를 가리키는 스마트 포인터를 돌려주는 함수이고 총 세 가지가 존재한다.(make_unique, make_shared, allocate_shared)

 

선호하는 이유

간결한 코드

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

make함수를 사용하면 코드의 길이부터 짧아지고, 타입선언 또한 한 번만 작성한다.

소스코드의 중복이 많으면 컴파일 시간이 늘어나며 일관성이 없는 코드로 진화할 수 있고, 버그로 이어지는 경우도 많은데 이 경우에서 make함수가 더 좋은 것을 알 수 있다.

 

예외 안정성

void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();

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

위의 코드는 문제가 없는 것 같지만 누수가 발생할 수 있다. 함수 호출 코드를 세분화하면 3단계로 나뉘는데,

new Widget -> shared_ptr<Widget>() -> computePriority()

widget 생성자와 shared_ptr생성자는 순서대로 진행되지만 그 중간에 computePriority 함수가 호출될 수 있고 여기서 예외가 발생하면 widget객체는 누수가 발생하게 된다.

make_shared함수를 사용하면 예외가 발생했더라도 widget객체는 shared_ptr로 관리되어 누수가 발생하지 않는다.

혹시라도 make함수를 사용할 수 없거나 사용이 부적합한 상황이라면 아래처럼 사용해서 예외 안정성 문제가 없도록 할 수 있다.

processWidget(std::move(spw), computePriority());

 

메모리 할당의 효율성

shared_ptr에는 제어블록이 생성된다.

이렇게 되면 객체를 생성하는데 한번, 제어블록을 생성하는데 한번, 총 두번의 메모리 할당이 발생하는데 make_shared는 한 번에 두 개 모두 할당하기 때문에 보다 효율적이다.

한 가지 문제로는 new와 delete연산자를 오버로딩하게 되면 원하지 않는 결과를 얻을 수 있어 주의해야 한다.

 

사용하지 못하는 상황

커스텀 삭제자를 사용해야 하는 상황

make함수는 커스텀 삭제자를 사용할 수 없다.

 

std::initializer_list

항목 7에서 설명하듯이 std::initializer_list를 받는 생성자와 받지 않는 생성자를 모두 가진 타입의 객체를 생성할 때, 생성자 인수들을 중괄호로 감싸면 오버로딩 해소 과정에서 std::initializer_list를 받는 버전이 선택되고, 괄호로 감싸면 std::initializer_list를 받지 않는 버전이 선택된다.

make함수들은 내부적으로 매개변수들을 완벽하게 전달할 때 중괄호가 아니라 괄호를 사용한다. 때문에 피지칭 객체를 중괄호 초기치로 생성하려면 반드시 new를 직접 사용해야 한다.

우회책으론 아래와 같은 방법이 있다.

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

 

shared_ptr에 대해 make함수가 부적합한 경우

클래스 중에는 자신만의 operator new와 operator delete를 정의하는 것들이 있다. 이런 클래스 고유 메모리 관리를 하는 경우 클래스의 객체와 정확히 같은 크기의 메모리만 할당, 해제하는 경우가 많은데 shared_ptr은 객체의 크기에 제어블록의 크기를 더한 것이기 때문에 잘 맞지 않는다.

그리고 앞서 말했듯이 make함수는 메모리를 한 번에 할당 한다. 즉, 해제도 한번에 해제해야 한다는 말이다.

보통 shared_ptr은 제어블록의 참조 카운트가 0이 되면 가리키던 피지칭 객체를 파괴하고, 메모리를 해제할 수 있다. 하지만 같은 피지칭 객체를 가리키는 weak_ptr이 모두 소멸되기 전(두 번째 참조 카운트가 0이 되기 전)에는 제어블록을 파괴할 수 없다.(weak_ptr들이 모두 만료되었는지 점검하기 위해 제어블록을 사용하기 때문)

결과적으로 make_shared를 통해 생성한 shared_ptr은 피지칭 객체와 제어블록이 하나의 메모리 조각에 있게 되어 weak_ptr들이 모두 소멸되기 전에는 피지칭 객체를 위해 할당한 메모리를 먼저 해제할 수 없다.

만약 피지칭 객체의 크기가 아주 크다면 메모리를 많이 소모할 수 있기 때문에 make를 사용하지 않는 것이 유리할 수 도 있다.

 

728x90
반응형