7 분 소요

  • [MEC++#18] 소유권 독점 자원의 관리에는 unique_ptr를 사용하라.(unique_ptr 참고)
  • [MEC++#22] PImpl 관용구를 사용할때에는 암시적으로 정의되는 특수 멤버 함수들을 구현 파일에서 정의하라.(unique_ptr을 이용한 PImpl 구현 참고)
    • 암묵적으로 inline소멸자~T()는, pImplraw 포인터를 삭제하기 전에 static_assert()를 검사하는데, 이 시점에 pImpl전방 선언이어서 불완전하여 delete가 해석되지 않을 수 있다. 따라서 pImpl 구현 코드가 있는 cpp에 T()::~T() = default; 넣어 소멸자 정의 시점을 변경해야 한다.(GCC에서는 재현되지 않습니다.)

개요

기존 auto_ptr은 복사/대입시 개체의 소유권을 이전하고, 소멸시 개체를 delete하는 스마트 포인터입니다.

하지만, 다음 문제로 인해 C++11에서 deprecate 되었습니다.

  1. 배열delete[]가 아닌 delete로 삭제합니다.(이러면 배열 요소들이 제대로 소멸되지 않습니다. delete와 delete[] 의 차이 참고)
  2. lvalue 복사 대입 연산시 소유권을 이전하는 이동 동작을 합니다.(이동 연산과 중복됩니다.)

C++11 부터는 상기 문제를 보완한 unique_ptr이 제공됩니다.

auto_ptr과 동일하게 소유권을 이전하는 스마트 포인터이며, 다음이 개선되었습니다.

  1. 일반 포인터는 delete하고, 배열delete[]합니다.
  2. 복사 생성자operator =(const T&)는 제공하지 않고, 이동 생성자operator =(const T&&)만 제공합니다. 즉, 이동 연산만 제공합니다.

다음은 사용예 입니다.

  1. 배열도 관리(unique_ptr<T[]>와 같이 []사용)할 수 있으며,
  2. 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은 복사 생성자가 없습니다.

다음처럼

  1. move()를 이용하여 이동 시키거나,
  2. 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을 구현한 예입니다.

선언부에서는,

  1. Impl전방 선언만 합니다.
  2. unique_ptrm_Impl 개체를 만듭니다. 스마트 포인터이므로, 소멸자에서 delete할 필요가 없습니다.
  3. 복사 생성자를 선언합니다.
  4. 복사 생성자가 정의되어 이동 생성자가 암시적으로 정의되지 않으므로 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);
};

정의부에서는,

  1. Impl을 구현합니다.
  2. 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을 생성합니다.
  1. [] 실수 방지

    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)};
    
  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 호출 순서에 따라 예외가 발생합니다.
    
    1. new T
    2. new U
    3. unique_ptr<T>
    4. 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) 
    

(C++23~) make_unique_for_override (작성중)

태그:

카테고리:

업데이트:

댓글남기기