[Modern C++] 항목 21: new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 4. 스마트 포인터
👉🏻 항목 21: new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
🔍 make 함수란?
std::make_shared는 C++11의 일부이지만std::make_unique는 C++14에서 표준 라이브러리에 포함되었다.- C++11을 사용하는 경우
std::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)...));
}
std::make_unique와std::make_shared는 임의의 개수와 타입의 인수들을 받아서 그것들을 생성자로 완벽 전달해서 객체를 동적으로 생성하고, 그 객체를 가리키는 스마트 포인터를 돌려준다.- 나머지 하나는
std::allocate_shared로,std::make_shared처럼 작동하되 첫 인수가 동적 메모리 할당에 쓰일 할당자 객체라는 점이 다르다.
✅ make 함수를 선호해야 하는 이유
이유 1: 코드 중복 방지
auto upw1(std::make_unique<Widget>()); // make 함수를 사용
std::unique_ptr<Widget> upw2(new Widget); // 사용하지 않음
auto spw1(std::make_shared<Widget>()); // make 함수를 사용
std::shared_ptr<Widget> spw2(new Widget); // 사용하지 않음
new를 사용하는 버전에서는 생성할 객체의 타입이 되풀이해서 나오지만, make 함수 버전은 그렇지 않다.- 타입을 여러 번 되풀이하는 것은 “코드 중복을 피하라”는 소프트웨어 공학의 핵심 교의와 충돌한다.
이유 2: 예외 안전성
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();
// ⚠️ 자원 누수 위험이 있음!
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
컴파일러가 다음과 같은 순서로 실행하는 목적 코드를 산출할 수도 있다.
"new Widget"을 실행한다.computePriority를 실행한다.std::shared_ptr생성자를 실행한다.
만일 단계 2에서 예외가 발생하면, 단계 1에서 동적으로 할당된 Widget이 새게 된다. std::shared_ptr가 아직 생성되지 않았기 때문이다.
// ✅ 자원 누수 위험이 없음
processWidget(std::make_shared<Widget>(), computePriority());
std::make_shared를 사용하면 std::make_shared가 먼저 호출되거나 computePriority가 먼저 호출되더라도 안전하다.
이유 3: 효율성 향상 (std::make_shared)
std::shared_ptr<Widget> spw(new Widget); // 메모리 할당 두 번 발생
new를 직접 사용해서std::shared_ptr를 생성하면 Widget 객체를 위한 메모리 할당과 제어 블록을 위한 또 다른 메모리 할당이 일어난다.
auto spw = std::make_shared<Widget>(); // 메모리 할당 한 번만 발생
std::make_shared를 사용하면 Widget 객체와 제어 블록 모두를 담을 수 있는 크기의 메모리 조각을 한 번에 할당한다.- 메모리 할당 호출 코드가 한 번만 있으면 되므로 프로그램의 정적 크기가 줄어든다.
- 실행 코드의 속도도 빨라진다.
std::make_shared의 효율성 분석은std::allocate_shared에도 그대로 적용된다.
⚠️ make 함수를 사용할 수 없거나 사용하지 않아야 하는 상황
한계 1: 커스텀 삭제자 지정 불가
auto widgetDeleter = [](Widget* pw) { … };
// make 함수로는 이런 일을 할 수 없다
std::unique_ptr<Widget, decltype(widgetDeleter)>
upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
한계 2: 중괄호 초기화 리스트 전달 불가
// 두 호출 모두 값이 20인 요소 열 개짜리 std::vector를 생성한다
auto upv = std::make_unique<std::vector<int>>(10, 20);
auto spv = std::make_shared<std::vector<int>>(10, 20);
- make 함수들은 내부적으로 매개변수들을 완벽 전달할 때 중괄호가 아니라 괄호를 사용한다.
- 중괄호 초기화 리스트를 사용하고 싶다면 반드시
new를 직접 사용해야 한다. - 우회책:
auto타입 추론을 이용해서std::initializer_list객체를 생성하고 make 함수에 넘겨주면 된다.
auto initList = { 10, 20 }; // std::initializer_list 객체를 생성
auto spv = std::make_shared<std::vector<int>>(initList);
한계 3: 클래스 고유 operator new/delete가 있는 경우
- 클래스 고유
operator new와operator delete가 있는 타입의 객체를 make 함수로 생성하는 것은 대체로 바람직하지 않다. std::allocate_shared가 요구하는 메모리 조각의 크기는 동적으로 할당되는 객체의 크기가 아니라 그 크기에 제어 블록의 크기를 더한 것이기 때문이다.
한계 4: 메모리 해제 시점 차이 (std::make_shared)
std::make_shared를 사용하면 객체와 제어 블록이 동일한 메모리 조각에 있어서, 마지막std::weak_ptr가 파괴되기 전까지는 객체가 차지하던 메모리도 해제될 수 없다.- 객체 타입이 상당히 크고, 마지막
std::shared_ptr의 파괴와 마지막std::weak_ptr의 파괴 사이의 시간 간격이 꽤 길다면, 객체가 파괴된 시점과 객체가 점유하던 메모리가 해제되는 시점 사이에 시간 지연이 생길 수 있다.
// std::make_shared 사용: 마지막 std::weak_ptr가 파괴될 때까지
// 객체와 제어 블록의 메모리가 모두 유지된다
auto pBigObj = std::make_shared<ReallyBigType>();
// new 직접 사용: 마지막 std::shared_ptr가 파괴되는 즉시
// 객체의 메모리가 해제될 수 있다
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
🛡️ make 함수를 쓸 수 없을 때 예외 안전한 코드 작성
std::make_shared를 사용할 수 없다면 Widget의 할당과 std::shared_ptr의 생성을 개별적인 문장으로 두는 것이 최선이다.
std::shared_ptr<Widget> spw(new Widget, cusDel); // 개별 문장으로 분리
processWidget(spw, computePriority()); // 정확하지만 최적은 아님; 본문 참고
processWidget(std::move(spw), computePriority()); // 예외 안전성과 효율성을 모두 갖춘 방식
std::shared_ptr의 경우 이동과 복사의 차이가 클 수 있다.std::shared_ptr를 복사하려면 참조 횟수를 원자적으로 증가해야 하지만, 이동할 때에는 참조 횟수를 조작할 필요가 없기 때문이다.
🧐 정리
new의 직접 사용에 비해, make 함수를 사용하면 소스 코드 중복의 여지가 없어지고, 예외 안전성이 향상되고,std::make_shared와std::allocate_shared의 경우 더 작고 빠른 코드가 산출된다.- make 함수의 사용이 불가능 또는 부적합한 경우로는 커스텀 삭제자를 지정해야 하는 경우와 중괄호 초기화 리스트를 전달해야 하는 경우가 있다.
std::shared_ptr에 대해서는 make 함수가 부적합한 경우가 더 있는데, 두 가지 예를 들자면 (1) 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우와 (2) 메모리가 넉넉하지 않은 시스템에서 큰 객체를 자주 다루어야 하고std::weak_ptr들이 해당std::shared_ptr들보다 더 오래 살아남는 경우이다.
댓글남기기