#16. [모던 C++ STL] unique_ptr, make_unique(C++11, C++14)
- [MEC++#18] 소유권 독점 자원의 관리에는 unique_ptr를 사용하라.(unique_ptr 참고)
- [MEC++#22]
PImpl
관용구를 사용할때에는 암시적으로 정의되는 특수 멤버 함수들을 구현 파일에서 정의하라.(unique_ptr을 이용한 PImpl 구현 참고)
- 암묵적으로 inline인 소멸자인
~T()
는,pImpl
의raw 포인터
를 삭제하기 전에 static_assert()를 검사하는데, 이 시점에pImpl
은 전방 선언이어서 불완전하여delete
가 해석되지 않을 수 있다. 따라서pImpl
구현 코드가 있는 cpp에T()::~T() = default;
넣어 소멸자 정의 시점을 변경해야 한다.(GCC에서는 재현되지 않습니다.)
- (C++11~) unique_ptr이 추가되어 소유권 이전용 스마트 포인터를 제공합니다. 기존 auto_ptr을 대체합니다. auto_ptr은 배열의 delete[] 미지원, 좌측값의 복사 대입 연산시 이동 동작을 하는 등의 사유로 deprecate 되었습니다.
- (C++11~) default_delete가 추가되어 스마트 포인터의
deleter
로 사용할 수 있습니다.- (C++14~) make_unique()가 추가되어 unique_ptr을 효율적으로 생성할 수 있습니다.
개요
기존 auto_ptr은 복사/대입시 개체의 소유권을 이전하고, 소멸시 개체를 delete하는 스마트 포인터입니다.
하지만, 다음 문제로 인해 C++11에서 deprecate 되었습니다.
- 배열을 delete[]가 아닌 delete로 삭제합니다.(이러면 배열 요소들이 제대로 소멸되지 않습니다. delete와 delete[] 의 차이 참고)
lvalue
복사 대입 연산시 소유권을 이전하는 이동 동작을 합니다.(이동 연산과 중복됩니다.)
C++11 부터는 상기 문제를 보완한 unique_ptr이 제공됩니다.
auto_ptr과 동일하게 소유권을 이전하는 스마트 포인터이며, 다음이 개선되었습니다.
- 일반 포인터는 delete하고, 배열은 delete[]합니다.
- 복사 생성자와
operator =(const T&)
는 제공하지 않고, 이동 생성자와operator =(const T&&)
만 제공합니다. 즉, 이동 연산만 제공합니다.
다음은 사용예 입니다.
- 배열도 관리(
unique_ptr<T[]>
와 같이[]
사용)할 수 있으며, c = d;
대신c = std::move(d);
를 하여 소유권을 이동시킵니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T {
public:
~T() {std::cout << "T : Destructor" << std::endl;}
};
std::unique_ptr<T> a{new T}; // 단일 개체. delete를 호출하여 1개가 소멸됩니다.
std::unique_ptr<T[]> b{new T[2]}; // 배열 개체. delete[] 를 호출하여 2개가 소멸됩니다.
std::unique_ptr<int> c{new int{0}};
std::unique_ptr<int> d{new int{1}};
// 소유권 이전으로 c는 d의 값을 갖고, d는 nullptr 이 됩니다.
// c = d; // (X) 컴파일 오류.
c = std::move(d); // 소유권 이전시 이동 연산을 사용합니다.
EXPECT_TRUE(*c == 1 && d == nullptr);
unique_ptr 멤버 함수
항목 | 내용 |
---|---|
constexpr unique_ptr() noexcept; (C++11~)explicit unique_ptr(T* p) noexcept; (C++11~)unique_ptr(T* p, deleter) noexcept; (C++11~)constexpr unique_ptr(nullptr_t) noexcept; (C++11~)unique_ptr(auto_ptr&&) noexcept; (C++11~C++17) |
nullptr이나 p 를 관리합니다. 이때 사용자 정의 deleter 를 사용할 수 있습니다. |
unique_ptr(const unique_ptr&) = delete; (C++11~) |
복사 생성자는 사용할 수 없습니다. |
unique_ptr(unique_ptr&& other) noexcept; (C++11~) |
이동 생성합니다. |
~unique_ptr() (C++11~) |
관리하는 개체를 delete 또는 delete[]합니다. |
unique_ptr& operator =(const unique_ptr&) = delete; (C++11~) |
복사 대입 연산자는 사용할 수 없습니다. |
unique_ptr& operator =(unique_ptr&& other) noexcept; (C++11~) |
이동 대입합니다.other 가 관리하는 개체를 this로 이동시킵니다. |
operator *() const noexcept; (C++11~) |
관리하는 개체의 참조자를 리턴합니다. |
operator ->() const noexcept; (C++11~) |
관리하는 개체의 포인터를 리턴합니다. |
operator [](size_t) const; (C++11~) |
배열을 관리하는 경우 각 요소 개체의 참조자를 리턴합니다. |
explicit operator bool() const noexcept; (C++11~) |
bool로 형변환시 nullptr 이면 false 를 리턴합니다. |
get() const noexcept; (C++11~) |
관리하는 개체의 포인터를 리턴합니다. |
swap(unique_ptr& other) noexcept; (C++11~) |
관리하는 개체를 other 와 바꿔치기 합니다. |
reset(T* p) noexcept; (C++11~) |
기존에 관리하던 개체를 해제하고 p 를 관리합니다. |
release() noexcept; (C++11~) |
관리하는 개체를 해제합니다. |
get_deleter() noexcept; (C++11~) |
관리하는 개체를 소멸시키는 deleter 를 리턴합니다. |
== (C++11~)!= (~C++20) |
관리하는 개체의 주소로 비교합니다. |
<, <=, >, >= (C++11~)<=> (C++20~) |
관리하는 개체의 주소로 비교합니다. |
<< (C++20~) |
관리하는 개체의 내용을 스트림에 출력합니다. |
unique_ptr을 활용한 함수 인자, 리턴 타입
auto_ptr의 경우와 동일하게(auto_ptr을 활용한 함수 인자, 리턴 타입 참고) unique_ptr을 사용하면 좀더 단단한 코딩 계약이 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
T GetData const; // (O, △) 부분적으로 비권장. 임시 객체를 리턴합니다. 하지만 혹시 멤버 변수를 리턴하는지 확인해 봐야 합니다.
const T& GetData() const; // (O) 멤버 변수를 리턴합니다. 수정하면 안됩니다.
T& GetData(); // (O) 멤버 변수를 리턴합니다.
T& GetData() const; // (△) 비권장. const 함수인데, 리턴받은 곳에서 멤버 수정이 가능합니다.
const T* GetData() const; // (△) 비권장. 리턴값을 delete 해야 하는건지 아닌건지 좀 모호합니다.
unique_ptr<T> GetData() const; // (O) 리턴값은 delete 해야합니다.
void f(T v); // (O, △) 부분적으로 비권장. 객체 전달입니다만, 복사 부하가 있을지 확인해 봐야 합니다.
void f(const T* p); // (△) 비권장. f에서 널검사는 하는지 좀 봐야 합니다.
void f(T* p); // (△) 비권장. f에서 널검사는 하는지 좀 봐야 합니다.
void f(const T& r); // (O) 널검사도 필요없고 참 좋습니다.
void f(T& r); // (O) 널검사도 필요없고 참 좋습니다.
void f(const unique_ptr<T>& p); // (△) 비권장. 쓸데없이 unique_ptr을 전달하지 않고, f(const T& r)이나, f(T& r)을 사용하는게 낫습니다.
void f(unique_ptr<T>& p); // (△) 비권장. p를 수정하겠다는 건지, unique_ptr을 수정하겠다는 건지 불분명 합니다. f(const T& r)이나, f(T& r)을 사용하는게 낫습니다.
void f(unique_ptr<T> p); // (O) new로 생성한 개체를 전달해야 합니다.
unique_ptr을 컨테이너 요소로 사용하기
unique_ptr은 복사 생성자가 없기 때문에 그냥 push_back()에 전달하면 컴파일 오류가 발생합니다.
1
2
3
4
5
6
std::vector<std::unique_ptr<int>> v;
std::unique_ptr<int> a{new int{10}};
std::unique_ptr<int> b{new int{20}};
v.push_back(a); // (X) 컴파일 오류. unique_ptr은 복사 생성자가 없습니다.
v.push_back(b); // (X) 컴파일 오류. unique_ptr은 복사 생성자가 없습니다.
다음처럼
- move()를 이용하여 이동 시키거나,
emplace_back()
을 이용하여 내부에서 개체를 생성합니다.(emplace()
계열의 함수는 컨테이너 요소 개체 생성을 위한 인수를 전달받아, 컨테이너 내에서 요소 개체를 생성한 뒤 삽입합니다. emplace() 계열 함수를 참고하세요.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::vector<std::unique_ptr<int>> v;
std::unique_ptr<int> a{new int{10}};
std::unique_ptr<int> b{new int{20}};
// v.push_back(a); // (X) 컴파일 오류. unique_ptr은 복사 생성자가 없습니다.
// v.push_back(b); // (X) 컴파일 오류. unique_ptr은 복사 생성자가 없습니다.
// move를 이용하여 이동시킵니다.
v.push_back(std::move(a)); // (O)
v.push_back(std::move(b)); // (O)
// 요소 개체(std::unique_ptr<int>)를 생성할 인자(int*)만 전달하고, 내부에서 개체(std::unique_ptr<int>)를 생성합니다.
v.emplace_back(new int{30});
EXPECT_TRUE(*v[0] == 10 && *v[1] == 20 && *v[2] == 30);
unique_ptr의 다형성
다형적 관계에 있는 경우 부모 클래스의 포인터로 변환됩니다.
다음과 같이 base = std::move(derived);
로 하여 std::unique_ptr<Derived>
를 std::unique_ptr<Base>
로 move()
할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
virtual int Func() const {return 1;}
};
class Derived : public Base {
public:
virtual int Func() const {return 2;}
};
std::unique_ptr<Base> base{new Base{}};
EXPECT_TRUE(base->Func() == 1);
std::unique_ptr<Derived> derived{new Derived{}};
EXPECT_TRUE(derived->Func() == 2);
base = std::move(derived); // unique_ptr<Derived>를 unique_ptr<Base>로 이동할 수 있습니다.
EXPECT_TRUE(base->Func() == 2);
// derived = std::move(base); // (X) 컴파일 오류. 반대는 안됩니다.
default_delete
unique_ptr은 관리하는 개체를 소멸시키는 deleter
를 사용자 정의 할 수 있습니다. 사용자 정의하는 방법은 shared_ptr Deleter를 참고하세요.
default_delete는 기본적으로 일반 포인터는 delete 로 소멸하고, 배열은 delete[]로 소멸시킵니다.
1
std::unique_ptr<int> a{new int{10}, std::default_delete<int>{}};
unique_ptr을 이용한 PImpl 구현
PImpl 이디엄에서 구현에 대한 상세 정보를 은닉하여, 컴파일 종속성을 최소화한 방법을 소개해 드렸는데요, unique_ptr을 이용하면 좀더 손쉽게 구현할 수 있습니다.
다음은 T
개체 내부에 중첩 클래스인 Impl
을 구현한 예입니다.
선언부에서는,
Impl
을 전방 선언만 합니다.- unique_ptr로
m_Impl
개체를 만듭니다. 스마트 포인터이므로, 소멸자에서 delete할 필요가 없습니다. - 복사 생성자를 선언합니다.
- 복사 생성자가 정의되어 이동 생성자가 암시적으로 정의되지 않으므로 default를 사용하여 명시적으로 정의합니다.(암시적 이동 생성자와 암시적 이동 대입 연산자의 default 정의 참고)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ----
// 선언에서
// ----
class T {
class Impl; // 포인터만 사용되므로 전방 선언으로 충분합니다.
std::unique_ptr<Impl> m_Impl; // 스마트 포인터여서 소멸자에서 delete 합니다.
public:
T(int x, int y);
T(const T& other); // 복사 생성자입니다.
T(T&& other) noexcept = default; // 복사 생성자를 정의했기 때문에 이동 생성자가 암시적으로 정의되지 않습니다. 따라서, 명시적으로 정의합니다.
bool IsValid() const;
int GetX() const;
int GetY() const;
void SetX(int x);
void SetY(int y);
};
정의부에서는,
Impl
을 구현합니다.T
를 구현합니다. 이때 복사 생성자는 unique_ptr이 관리하는 개체의 복제본을 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ----
// 정의에서
// ----
class T::Impl {
public:
int m_X;
int m_Y;
Impl(int x, int y) : m_X(x), m_Y(y) {}
};
T::T(int x, int y) : m_Impl{std::unique_ptr<T::Impl>{new Impl{x, y}}} {}
T::T(const T& other) : m_Impl{other.m_Impl ? new Impl(*other.m_Impl) : nullptr} {}
bool T::IsValid() const {return m_Impl ? true : false;}
int T::GetX() const {return m_Impl->m_X;}
int T::GetY() const {return m_Impl->m_Y;}
void T::SetX(int x) {m_Impl->m_X = x;}
void T::SetY(int y) {m_Impl->m_Y = y;}
T a{10, 20};
EXPECT_TRUE(a.IsValid() && a.GetX() == 10 && a.GetY() == 20);
T b{a}; // 복사 생성합니다.
EXPECT_TRUE(b.IsValid() && b.GetX() == 10 && b.GetY() == 20);
a.SetX(1);
a.SetY(2);
EXPECT_TRUE(a.IsValid() && a.GetX() == 1 && a.GetY() == 2);
EXPECT_TRUE(b.IsValid() && b.GetX() == 10 && b.GetY() == 20);
T c{std::move(b)};
EXPECT_TRUE(b.IsValid() == false); // 이동되어 Impl은 무효화되었습니다.
EXPECT_TRUE(c.IsValid() && c.GetX() == 10 && c.GetY() == 20);
(C++14~) make_unique()
make_unique()는 unique_ptr을 생성합니다. 이때 T
개체의 생성자 인자를 전달받아 내부적으로 T
개체를 생성합니다.
1
2
3
4
5
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args); // args 를 인자로 받는 T 개체를 관리하는 unique_ptr을 생성합니다.
template< class T >
unique_ptr<T> make_unique(std::size_t size); // size 개의 배열 개체를 관리하는 unique_ptr을 생성합니다.
-
[]
실수 방지make_unique()를 사용하면
[]
실수를 방지할 수 있습니다.1 2 3 4
std::unique_ptr<T> a{new T}; std::unique_ptr<T> b{new T[2]}; // (△) 비권장. unique_ptr<T[]>인데, 실수로 []을 빼먹었지만 컴파일 됩니다. //std::unique_ptr<T> c{std::make_unique<T>(2)}; // (X) 컴파일 오류 std::unique_ptr<T[]> d{std::make_unique<T[]>(2)};
-
예외 보증 향상
또한, 예외에 좀더 안전합니다. 다음 코드는 예외를 보증하는 듯 하지만,
1 2 3 4 5
class T {}; class U {}; void Func(std::unique_ptr<T> t, std::unique_ptr<U> u) {} Func(std::unique_ptr<T>{new T}, std::unique_ptr<U>{new U}); // (△) 비권장. new T, new U 호출 순서에 따라 예외가 발생합니다.
new T
new U
unique_ptr<T>
unique_ptr<U>
의 순서로 실행될 경우
new U
에서 예외가 발생할 경우new T
는 소멸되지 않습니다. 따라서 make_unique()를 사용하는게 좋습니다.1 2 3 4 5
class T {}; class U {}; void Func(std::unique_ptr<T> t, std::unique_ptr<U> u) {} Func(std::make_unique<T>(), std::make_unique<U>()); // (O)
댓글남기기