[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
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를 사용하지 않는 것이 유리할 수 도 있다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
2022.09.24std::move와 std::forward는 그냥 캐스팅을 수행하는 함수 템플릿이다. std::move 함수의 이름만 보면 std::move가 이동을 수행해 줄 것처럼 보이지만 위에서 말했던 것처럼 사실 타입 캐스팅이 전부이다. 그런데도 이 함수의 이름이 move인 이유는 rvalue로 캐스팅을 하기 때문이고, 아래의 코드는 std::move를 구현한 코드이다. // C++11 template typename remove_reference::type&& move(T&& param) { using ReturnType = typename remove_reference::type&&; return static_cast(param); } // C++14 더 단순해진 move구현 template decltype(… -
[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
2022.09.18Pimpl이란 pointer to implementation을 의미한다. // 기존 class Widget { public: Widget(); …. private: std::string name; std:;vector data; Gadget g1, g2, g3; }; // Pimpl class Widget { public: Widget(); ~Widget(); …. private: struct Impl; Impl *pImpl; }; Pimpl 적용 코드를 보면 string, vector, Gadget을 선언하고 있지 않아 #include 할 필요가 없어져 컴파일 속도가 빨라지게 되었다. 이렇게 선언만 하고 정의를 하지 않는 형식을 불완전한 형식이라고 하며, 구현부에 작성하면 된다. #include "… -
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
2022.09.18weak_ptr weak_ptr는 언제든 소멸될 수 있는 객체를 사용할 때 쓰는 포인터이다. 대체로 weak_ptr은 shared_ptr을 이용해서 생성하지만 reference count를 증가시키지 않는다. 즉, 객체의 소멸에 관여하지 않는다. 대상을 잃은 weak_ptr은 만료되며 만료 여부는 멤버 함수 expired가 돌려주는 값으로 판단할 수 있다. 만료되지 않은 weak_ptr이라고 해도 역참조 연산이 없기 때문에 피지칭 객체에 직접 접근하는 것은 불가능하다. 만일 역참조 연산이 가능하게 하도록 한다고 하면, 사용하려고 하는 순간 이미 객체가 소멸되어 미정의 행동이 나올 수도 있다. // std::weak_ptr가 가리키는 피지칭 객체를 역참조 하려면 std::weak_ptr로부터 std::sh… -
[Effective Modern C++] 19. 소유권 공유 자원의 관리에는 std::shared_ptr를 사용하라
[Effective Modern C++] 19. 소유권 공유 자원의 관리에는 std::shared_ptr를 사용하라
2022.09.18shared_ptr의 특징 shared_ptr은 unique_ptr과 다르게 여러 객체에서 한 객체를 소유할 수 있다. shared_ptr은 객체의 소멸 시점을 관리하기 위하여 reference count를 사용하는데, 이 count가 0이 되면 메모리를 해제한다. shared_ptr의 내부 구현은 다음과 같다. shared_ptr의 크기는 raw포인터의 두배이다. raw포인터의 크기와 reference count를 가리키는 포인터를 가지고 있어야 하기 때문이다. 참조 횟수를 담을 메모리를 반드시 동적으로 할당해야 한다. reference count를 가리키는 객체는 공유되는 자원이기 때문에 동적으로 할당해야 한다. 참조 횟수의 증가와 감소가 반드시 원자적(atomic) 연산이어야 한다. 멀티스레드 프로…
댓글을 사용할 수 없습니다.