19 분 소요

개요

C++11 부터는 소유권을 이전하는 스마트 포인터인 auto_ptrunique_ptr 외에, 개체의 소유권을 공유하는 스마트 포인터인 shared_ptr 도 제공합니다.

항목 내용
shared_ptr (C++11~) 소유권 공유용 스마트 포인터입니다.
make_shared() (C++11~) shared_ptr을 효율적으로 생성합니다.
weak_ptr (C++11~) shared_ptr상호 참조 문제 해결용 스마트 포인터입니다.
enable_shared_from_this (C++11~) shared_ptr이 관리하는 개체로부터 shared_ptr을 만듭니다.
owner_less() (C++11~) 소유권 개체의 주소로 비교합니다.
shared_ptr 형변환 (C++11~) shared_ptr 형변환(const_pointer_cast(), static_pointer_cast(), dynamic_pointer_cast())가 추가되어 shared_ptr의 관리 개체를 형변환 할 수 있습니다.
bad_weak_ptr (C++11~) shared_ptr에서 잘못된 weak_ptr을 사용할때 발생하는 예외입니다.

shared_ptr

unique_ptr은 소유권을 이전합니다만, shared_ptr은 개체의 소유권을 공유하는 스마트 포인터입니다.

  1. 소유권이 공유될때마다 참조 카운트를 증가시키고,
  2. 스마트 포인터가 소멸될때마다 참조 카운트를 감소시켜 0이 될때 delete합니다.

다음 코드를 보면 참조 카운트가

  1. T{}로 생성한 개체를 a에 저장할 때 1이 되고,
  2. 이를 b복사 생성할 때 2가 되고,
  3. c복사 생성할때 3이 되고,
  4. 스마트 포인터가 소멸할 때 1씩 감소하는 것을 알 수 있습니다.

또한 a, b, c에서 각각 관리 대상 개체를 수정하더라도 서로 개체를 공유하므로 값이 동일한 것을 알 수 있습니다.

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
class T {
public:
    int m_Val;
public:
    T() {std::cout << "T : Constructor" << std::endl;}
    ~T() {std::cout << "T : Destructor" << std::endl;}
};
std::shared_ptr<T> a{new T{}};
EXPECT_TRUE(a.use_count() == 1); 

std::shared_ptr<T> b{a};
EXPECT_TRUE(a.use_count() == 2 && b.use_count() == 2); 
{
    std::shared_ptr<T> c{a};
    EXPECT_TRUE(a.use_count() == 3 && b.use_count() == 3 && c.use_count() == 3);  

    // a, b, c가 관리하는 T는 서로 공유합니다.
    a->m_Val = 1; 
    EXPECT_TRUE(a->m_Val == 1 && b->m_Val == 1 && c->m_Val == 1); 

    b->m_Val = 2;
    EXPECT_TRUE(a->m_Val == 2 && b->m_Val == 2 && c->m_Val == 2); 

    c->m_Val = 3;
    EXPECT_TRUE(a->m_Val == 3 && b->m_Val == 3 && c->m_Val == 3);              
} // 유효 범위를 벗어났기 때문에 c가 소멸됨
// c가 소멸되었으므로 2
EXPECT_TRUE(a.use_count() == 2 && b.use_count() == 2);

실행 시키면 다음과 같이 생성자와 소멸자는 1회 호출됩니다.

1
2
T : Constructor
T : Destructor

(C++17~) shared_ptr의 배열 지원이 추가되었습니다.

shared_ptr의 제어 블록(Control Block)

shared_ptr은 내부적으로 제어 블록을 별도로 할당하며, 다음 정보를 관리합니다.

  1. 관리 대상 개체 포인터
  2. shared_ptr 참조 카운트
  3. weak_ptr 참조 카운트
  4. 관리 대상 개체의 deleter(있는 경우)
  5. 관리 대상 개체의 allocator(있는 경우)
1
2
3
4
class T {};

std::shared_ptr<T> a{new T{}};
std::shared_ptr<T> b{a};

상기 코드의 상황을 그림으로 도식화 하면 다음과 같습니다. ab는 같은 제어 블록을 공유함으로서, 관리 대상 개체 포인터와 참조 카운트를 공유합니다. 포인터 1개를 관리하는 것 치고는 추가 메모리가 제법 필요하죠. 포인터 관리 편의성과 메모리 용량 최적화 사이에서 신중하게 선택해서 사용해야 됩니다.

image

또한, shared_ptrAllocatorDeleter를 별도로 지정할 수 있는데요, 별도로 지정하면, 이 정보도 제어 블록에 포함됩니다.

shared_ptr 멤버 함수

항목 내용
constexpr shared_ptr() noexcept; (C++11~)

explicit shared_ptr(T* p); (C++11~)
shared_ptr(T* p, deleter); (C++11~)
shared_ptr(T* p, deleter, allocator); (C++11~)

constexpr shared_ptr(nullptr_t) noexcept; (C++11~)
shared_ptr(nullptr_t p, deleter); (C++11~)
shared_ptr(nullptr_t p, deleter, allocator); (C++11~)

explicit shared_ptr(const weak_ptr&); (C++11~)
shared_ptr(unique_ptr&&); (C++11~)

별칭 생성자
shared_ptr(const shared_ptr& other, element_type* p) noexcept; (C++11~)

shared_ptr( auto_ptr&&); (C++11~C++17)
nullptr이나 p를 공유하며, 참조 카운트를 증가시킵니다. 이때 사용자 정의 deleterallocator 를 사용할 수 있습니다. weak_ptrunique_ptr로 생성할 수도 있습니다.
shared_ptr(const shared_ptr& other) noexcept; (C++11~) 개체와 소유권을 공유하고 참조 카운트를 증가시킵니다.
shared_ptr(const shared_ptr&& other) noexcept; (C++11~) 이동 생성합니다.
~shared_ptr(); (C++11~) 관리하던 개체의 참조 카운트를 감소시키고, 0이 되면 delete또는 delete[](C++17~)합니다.
shared_ptr& operator =(const shared_ptr& other) noexcept; (C++11~) other 개체와 소유권을 공유하고 참조 카운트를 증가시킵니다.
shared_ptr& operator =(shared_ptr&& r) noexcept; (C++11~) 이동 대입합니다.
other가 관리하는 개체를 this로 이동시킵니다.
operator *() const noexcept; (C++11~) 관리하는 개체의 참조자를 리턴합니다.
operator ->() const noexcept; (C++11~) 관리하는 개체의 포인터를 리턴합니다.
operator [](ptrdiff_t) const; (C++17~) 배열을 관리하는 경우 각 요소 개체의 참조자를 리턴합니다.
explicit operator bool() const noexcept; (C++11~) bool로 형변환시 nullptr 이면 false를 리턴합니다.
get() const noexcept; (C++11~) 관리하는 개체의 포인터를 리턴합니다.
swap(shared_ptr& other) noexcept; (C++11~) 관리하는 개체를 other와 바꿔치기 합니다.
reset(T* p); 기존에 관리하던 개체를 해제하고 p를 관리합니다.
use_count() const noexcept; (C++11~) 참조 카운트를 리턴합니다.
unique() const noexcept;(C++11~C++17) 다른 shared_ptr로 관리하는지 검사합니다. use_count() == 1과 같습니다.
lock() const noexcept; (C++11~) 관리하는 개체 접근을 위해 임시 shared_ptr을 생성합니다.
owner_before() const noexcept; (C++11~) 소유권 개체로 < 비교를 합니다.
get_deleter() (C++11~) 관리하는 개체를 소멸시키는 deleter를 리턴합니다.
== (C++11~)
!= (C++11~C++20)
관리하는 개체의 주소로 비교합니다.
<, <=, >, >= (C++11~C++20)
<=> (C++20~)
관리하는 개체의 주소로 비교합니다.
<< (C++11~) 관리하는 개체의 내용을 스트림에 출력합니다.

shared_ptr 소유권 분쟁

shared_ptr을 잘못 사용하면 소유권 분쟁이 생길 수 있습니다.

다음은 올바른 사용예입니다. ba로 부터 생성했기 때문에 제어 블록을 공유하고, 참조 카운트도 공유합니다.

1
2
3
4
5
6
// 올바른 예
std::shared_ptr<int> a{new int{}};
EXPECT_TRUE(a.use_count() == 1); 

std::shared_ptr<int> b{a};
EXPECT_TRUE(a.use_count() == 2 && b.use_count() == 2); // a, b 가 같은 제어 블록을 공유합니다.

다음은 잘못된 사용예입니다. p로부터 각각 a, b를 생성했기 때문에 제어 블록을 각자 생성하고, 각자 참조 카운트를 사용합니다. 따라서, 유효 범위를 벗어나면 각자 pdelete하고, 결국 p는 2번 delete하여 오동작을 하게 됩니다.

1
2
3
4
5
6
7
// 잘못된 예
int* p = new int{10};
std::shared_ptr<int> a{p}; // p를 관리하는 제어 블록을 생성합니다.
std::shared_ptr<int> b{p}; // (X) 오동작. p를 관리하는 제어 블록을 생성합니다.

EXPECT_TRUE(a.use_count() == 1); // a, b가 각각의 제어 블록을 사용하기 때문에 참조 카운트는 각각 1입니다.
EXPECT_TRUE(b.use_count() == 1);

상기와 같이 포인터를 생성한 뒤에 shared_ptr에 전달하는 건 좋지 않은 코딩 습관입니다. std::shared_ptr<int> a{new int{}};와 같이 shared_ptr 생성자에 바로 개체를 생성해서 전달하거나, make_shared()를 이용하는게 좋습니다.

make_shared()

make_shared()shared_ptr을 생성합니다. 이때 T개체의 생성자 인자를 전달받아 내부적으로 T개체를 생성합니다.

1
2
template<typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args);

make_shared()를 사용하면 다음 장점이 있습니다.

  1. 메모리 효율성 향상

    make_shared()를 이용하면 좀더 메모리 효율적으로 shared_ptr을 사용할 수 있습니다.

    다음 코드는 T개체를 메모리에 할당하고, 내부적으로 제어 블록을 메모리에 할당하여 메모리를 2회 할당하지만,

    1
    2
    3
    4
    5
    
     class T {
     public:
         explicit T(int) {}    
     };
     std::shared_ptr<T> a{new T{10}}; // (△) 비권장. T를 할당하고, 내부적으로 제어 블록을 할당
    

    다음처럼 make_shared()를 사용하면 T제어 블록의 크기를 합하여 메모리를 1회 할당합니다.

    1
    
     std::shared_ptr<T> b = std::make_shared<T>(10); // T + 제어 블록 크기만큼 할당
    
  2. 코드 간결성 향상

    또한 코드가 간결해지는 효과도 있습니다. shared_ptr<T>{new T{10}} 보다는 make_shared<T>(10)가 간결합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     class T {
     public:
         explicit T(int) {}    
     };
     std::vector<std::shared_ptr<T>> v;
    
     v.push_back(std::shared_ptr<T>{new T{10}});
     v.push_back(std::make_shared<T>(10)); // 구문이 좀더 간결합니다.
     v.push_back(std::shared_ptr<T>{}); // nullptr
    
     EXPECT_TRUE(v.size() == 3 && v[2] == nullptr);
    
  3. 예외 보증 향상

    또한, 예외에 좀더 안전합니다. 다음 코드는 예외 보증을 하는 듯 하지만,

    1
    2
    3
    4
    5
    
     class T {};
     class U {};
     void Func(std::shared_ptr<T> t, std::shared_ptr<U> u) {}
    
     Func(std::shared_ptr<T>{new T}, std::shared_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_shared()를 사용하는게 좋습니다.

    1
    2
    3
    4
    5
    
     class T {};
     class U {};
     void Func(std::shared_ptr<T> t, std::shared_ptr<U> u) {}
    
     Func(std::make_shared<T>(), std::make_shared<U>()); // (O) 
    

    (C++20~) make_shared()의 배열 지원이 추가되었습니다.

make_shared() 와 initializer_list

vectorinitializer_list인자로 받는 생성자가 있어서 생성자 호출시 (){}가 달랐습니다.(기존 생성자와 initializer_list 생성자와의 충돌 참고)

1
2
3
4
5
std::vector<int> v(2); // 요소 갯수가 2개인 vector 생성
EXPECT_TRUE(v.size() == 2 && v[0] == 0 && v[1] == 0);

std::vector<int> v_11{2}; // initializer_list 버전 호출. 요소값이 2인 vector 생성
EXPECT_TRUE(v_11.size() == 1 && v_11[0] == 2);  

make_shared()는 생성자 인자를 전달받아 내부적으로 개체를 생성하는데요, ()형태로 생성자를 호출합니다. 따라서, initializer_list를 사용한 생성자를 호출하고 싶은 경우에는 다음과 같이 initializer_list변수를 만들어 전달해줘야 합니다.

1
2
3
4
5
6
auto v{std::make_shared<std::vector<int>>(2)}; // 요소 갯수가 2개인 vector 생성
EXPECT_TRUE(v->size() == 2 && (*v)[0] == 0 && (*v)[1] == 0);

std::initializer_list<int> list{2};
auto v_11{std::make_shared<std::vector<int>>(list)}; // initializer_list 버전 호출. 요소값이 2인 vector 생성
EXPECT_TRUE((*v_11).size() == 1 && (*v_11)[0] == 2); 

allocate_shared()

make_shared()와 동일하며, 추가로 Allocator인자로 받습니다.

enable_shared_from_this

항상 shared_ptr로만 생성할 수 있도록 생성자 접근을 차단하고 Create()함수를 제공한다고 합시다.

1
2
3
4
5
6
7
8
9
10
11
class T {
private:
    T() {} // private로 만들어서 생성자 접근 차단
public:
    static std::shared_ptr<T> Create() {
        return std::shared_ptr<T>{new T{}};      
    }  
};

std::shared_ptr<T> a{new T{}}; // (X) 컴파일 오류. 생성자가 private
std::shared_ptr<T> a{T::Create()};

이제, T{} 개체를 공유하는 다른 shared_ptr을 만들려면, 다음처럼 하면 됩니다.

1
2
3
4
// a의 제어 블록을 공유합니다.
std::shared_ptr<T> b{a}; 

EXPECT_TRUE(a.use_count() == 2);

그런데, 상기 방법은 개체를 shared_ptr 형태로 알고 있어야 가능합니다. 그래서, 원본 shared_ptr을 필요한 곳까지 인수로 전달해 줘야 합니다.

하지만 이렇게 항상 shared_ptr을 인수로 전달하는 건 부담이죠. 그래서 포인터나 참조자로 전달했다가 뒤늦게 shared_ptr을 만들어야 하는 경우가 오면 낭패입니다.

만약 관리하는 개체로부터 shared_ptr을 만들기 위해 다음과 같이 작성한다면, ba제어 블록을 공유하지 않고 새로운 제어 블록을 만들어 관리하기 때문에 추후 소유권 분쟁이 발생합니다.

1
2
3
4
5
// (X) 오동작. 동일한 개체를 각각의 제어 블록으로 관리합니다. 소유권을 공유하지 않았기 때문에 소멸시 각각 delete 하여 소유권 분쟁이 발생합니다.
// a가 관리하는 포인터를 이용하여 새로운 제어 블록을 만듭니다.
std::shared_ptr<T> b{a.get()}; 

EXPECT_TRUE(a.use_count() == 1 && b.use_count() == 1);

다행스럽게도 이런 경우를 위하여 enable_shared_from_this를 제공하고 있습니다.

enable_shared_from_thisshared_ptr이 관리하는 개체로부터 shared_ptr을 생성할 수 있습니다. 다만, shared_ptr이 미리 만들어져 있어야 합니다.

  1. enable_shared_from_thispublic상속합니다.(protectedprivate상속은 동작하지 않습니다.)
  2. shared_from_this()를 호출합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class T : public std::enable_shared_from_this<T> {
private:
    T() {} // private로 만들어서 생성자 접근 차단
public:
    // this 포인터로부터 shared_ptr을 생성합니다.
    std::shared_ptr<T> GetPtr() {
        return shared_from_this();
    }
    static std::shared_ptr<T> Create() {
        return std::shared_ptr<T>{new T{}};      
    }  
};

std::shared_ptr<T> a{T::Create()}; // shared_ptr이 만들어 졌습니다. 이제 shared_from_this()를 사용할 수 있습니다.
T* t{a.get()};
std::shared_ptr<T> b{t->GetPtr()}; // 관리하는 개체로부터 shared_ptr을 생성합니다.

EXPECT_TRUE(a.use_count() == 2);   

shared_ptr을 이용한 복사 생성자, 복사 대입 연산자

복사 대입 연산자까지 지원하는 스마트 포인터에서 개체의 복사 생성이나 복사 대입시 소유권 분쟁이 없도록 포인터를 복제해서 관리하는 IntPtr을 소개해 드렸습니다.

shared_ptr을 이용하면 개체의 복사 생성이나 복사 대입 연산시 포인터를 공유하는 방식으로 소유권 분쟁을 해결할 수 있습니다.

다음 코드는,

  1. shared_ptr멤버 변수로 사용합니다.
  2. new로 생성된 Data인자로 전달받기 위해 생성자에서 unique_ptr인자로 사용합니다.
  3. 복사 생성자에서 other개체의 각 멤버의 복사 생성자를 호출합니다.
  4. shared_ptr에서 참조 카운트가 0이 되면 소멸시킬 것이므로 소멸자에서는 별다른 작업을 하지 않습니다.
  5. 복사 대입 연산자에서는 각 멤버를 복사 대입합니다.

    이때 shared_ptr복사 대입 연산자noexcept로 선언되어 있어서 예외를 발생시키지 않습니다. 포인터 복사와 참조 카운트 변경만 있으니까 예외 발생할게 없는거죠.

    1
    
     shared_ptr& operator=( const shared_ptr& r ) noexcept; 
    

    예외 발생을 하지 않으므로, 굳이 Swap() 을 사용할 필요가 없어서 코드가 간결합니다.(멤버 변수가 2개 이상인 경우 스마트 포인터와 복사 대입 연산자와의 호환성에서 개체 대입시 예외가 발생할 수 있으므로, Swap()을 사용하는게 예외에 안전하다고 한 바 있습니다.)

코드를 테스트 해보면, T개체를 복사 생성, 복사 대입하는 경우 소유권 분쟁은 없으며, Data를 서로 공유한다는 것을 알 수 있습니다.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Data {
    int m_Val;
public:
    explicit Data(int val) : m_Val(val) {}
    int GetVal() const {return m_Val;}
    void SetVal(int val) {m_Val = val;}
};
class T {
public:    
    std::shared_ptr<Data> m_Data1;
    std::shared_ptr<Data> m_Data2;
public:
    // new로 생성된 Data를 받기 위해 unique_ptr을 인자로 사용합니다.
    T(std::unique_ptr<Data> data1, std::unique_ptr<Data> data2) :
        m_Data1(std::move(data1)),
        m_Data2(std::move(data2)) {}

    // 컴파일러의 암시적 버전과 동일합니다.
    T(const T& other) : 
        m_Data1(other.m_Data1),
        m_Data2(other.m_Data2) {}

    // 컴파일러의 암시적 버전과 동일합니다.
    ~T() {}

    // 컴파일러의 암시적 버전과 동일합니다.
    T& operator =(const T& other) {
        // shared_ptr& operator=( const shared_ptr& r ) noexcept; 입니다. 
        // 포인터 복사와 참조 카운트 변경만 있으므로 예외 발생은 없습니다.
        m_Data1 = other.m_Data1; 
        m_Data2 = other.m_Data2; 
        
        return *this;
    }
};

T a{std::unique_ptr<Data>{new Data{1}}, std::unique_ptr<Data>{new Data{1}}};
T b{std::unique_ptr<Data>{new Data{2}}, std::unique_ptr<Data>{new Data{2}}};
T c{std::unique_ptr<Data>{new Data{3}}, std::unique_ptr<Data>{new Data{3}}};

EXPECT_TRUE(a.m_Data1.use_count() == 1 && b.m_Data1.use_count() == 1 && c.m_Data1.use_count() == 1);

//복사 생성합니다. a와 d는 동일 Data 입니다.
T d{a};
EXPECT_TRUE(a.m_Data1.use_count() == 2 && b.m_Data1.use_count() == 1 && c.m_Data1.use_count() == 1 && d.m_Data1.use_count() == 2);

// a의 Data 값을 수정하면 d의 값도 수정됩니다.
a.m_Data1->SetVal(10);
EXPECT_TRUE(a.m_Data1->GetVal() == 10 && b.m_Data1->GetVal() == 2 && c.m_Data1->GetVal() == 3 && d.m_Data1->GetVal() == 10);

// 복사 대입 합니다. a, b, d는 동일 Data 입니다.
b = d;
EXPECT_TRUE(a.m_Data1.use_count() == 3 && b.m_Data1.use_count() == 3 && c.m_Data1.use_count() == 1 && d.m_Data1.use_count() == 3);

// a의 Data 값을 수정하면 b, d의 값도 수정됩니다.
a.m_Data1->SetVal(20);
EXPECT_TRUE(a.m_Data1->GetVal() == 20 && b.m_Data1->GetVal() == 20 && c.m_Data1->GetVal() == 3 && d.m_Data1->GetVal() == 20);        

상기 코드를 보면, 복사 생성자, 소멸자, 복사 대입 연산자가 모두 암시적 버전과 동일한 것을 알 수 있습니다.

즉, 다음처럼 훨씬 더 간결하게 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T {
public:    
    std::shared_ptr<Data> m_Data1;
    std::shared_ptr<Data> m_Data2;
public:
    // new로 생성된 Data를 받기 위해 unique_ptr을 인자로 사용합니다.
    T(std::unique_ptr<Data> data1, std::unique_ptr<Data> data2) :
        m_Data1(std::move(data1)),
        m_Data2(std::move(data2)) {}

    // 컴파일러의 암시적 버전과 동일합니다.
    T(const T& other) = default;
    ~T() = default;
    T& operator =(const T& other) = default;
};

shared_ptr Deleter

shared_ptr은 관리하는 개체를 소멸시키는 deleter를 사용자 정의 할 수 있습니다.

다음 코드에서는 람다 표현식으로 deleter를 전달했는데요, 함수, 함수자 모두 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
std::shared_ptr<int> a{
    new int{10}, 
    [](int* obj) { // 람다 표현식외에 함수, 함수자 모두 가능합니다.
        delete obj;
        std::cout << "deleter test" << std::endl;
    }
};  

std::shared_ptr<int>b{a};
*b = 20;
EXPECT_TRUE(*a == 20 && *b == 20);

shared_ptr Allocator

shared_ptr은 관리하는 개체를 메모리에 할당하는 allocator를 사용자 정의 할 수 있습니다.

allocator의 구현 방법은 할당자(Allocator)를 참고하시기 바랍니다.

1
2
3
4
5
6
7
8
std::shared_ptr<int> a{
    new int {10},
    std::default_delete<int>{}, // 기본 deleter
    std::allocator<int>{} // 기본 allocater
};
std::shared_ptr<int>b{a};
*b = 20;
EXPECT_TRUE(*a == 20 && *b == 20);

shared_ptr 별칭 생성자

shared_ptr별칭 생성자(Aliasing Constructor)라는 좀 특별한 생성자를 가지고 있습니다. 이 생성자는 소유권 대상인 개체와 접근 대상 개체가 다른 경우에 사용합니다.

보통,

1
std::shared_ptr<T> data{new T{}};

와 같이 생성하면, T 개체의 소유권에 대해 참조 카운팅을 하고, *, ->등으로 T개체에 접근하는데요, 별칭 생성자(Aliasing Constructor)로 생성하면, 서로 다른 개체에 대해 참조 카운팅과 접근을 할 수 있습니다.

선언은 다음과 같은데요, other개체에 참조 카운팅을 하고, p 개체를 접근합니다. 따라서, p의 수명 주기가 전적으로 other에 종속적일 때만 안전하게 사용 가능합니다. 예를 들어 pother멤버 변수 일때 처럼요.

1
shared_ptr(const shared_ptr& other, element_type* p) noexcept;

다음 코드는 별칭 생성자(Aliasing Constructor)의 사용 예입니다.

  1. MainData에서 멤버 변수m_SubData를 가지고 있으며,
  2. 별칭 생성자(Aliasing Constructor)shared_ptr<SubData> subData 개체를 만듭니다.
  3. mainData1, mainData2, subData 가 참조 카운트를 공유하며,
  4. subData의 개체 접근을 통해 mainData1m_SubData를 수정합니다.
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
class SubData {
    int m_Val{0};
public:
    SubData() {std::cout << "SubData : Constructor" << std::endl;}
    ~SubData() {std::cout << "SubData : Destructor" << std::endl;}

    int GetVal() const {return m_Val;}
    void SetVal(int val) {m_Val = val;}
};
class MainData {
    SubData m_SubData;
public:
    MainData() {std::cout << "MainData : Constructor" << std::endl;}
    ~MainData() {std::cout << "MainData : Destructor" << std::endl;}            
    SubData& GetSubData() {return m_SubData;}        
};

std::shared_ptr<MainData> mainData1{new MainData{}};
std::shared_ptr<MainData> mainData2{mainData1};

EXPECT_TRUE(mainData1.use_count() == 2 && mainData2.use_count() == 2);

// 별칭 생성자를 만듭니다. 생성과 소멸은 MainData로 하지만, 참조하는 포인터는 SubData입니다.
std::shared_ptr<SubData> subData{mainData1, &(mainData1->GetSubData())};

// mainData1, mainData2, subData에서 사용하므로 3입니다.
EXPECT_TRUE(mainData1.use_count() == 3 && mainData2.use_count() == 3 && subData.use_count() == 3);

// subData를 수정하면 MainData에서도 같이 수정됩니다.
subData->SetVal(10);
EXPECT_TRUE(mainData1->GetSubData().GetVal() == 10 && mainData2->GetSubData().GetVal() == 10 && subData->GetVal() == 10);

실행 결과를 보면 생성한 만큼 잘 소멸하는 것을 알 수 있습니다.

1
2
3
4
SubData : Constructor
MainData : Constructor
MainData : Destructor
SubData : Destructor

owner_before() 비교

shared_ptr의 대소 비교 연산자는 관리하는 개체의 주소를 비교합니다. 보통은 소유권을 가진 개체와 관리하는 개체가 같으므로 상관 없습니다만, 별칭 생성자를 이용하면 이둘이 서로 다르므로, 소유권을 가진 개체로 비교 하고 싶을 때는 owner_before()를 사용합니다.

다음 코드에서 별칭 생성자로 c를 만들고, <owner_before()의 차이를 나타내었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::shared_ptr<int> a{new int{10}};
std::shared_ptr<int> b{a};

// a, b는 동일한 개체이므로 < 비교시 !(a < b) && !(b < a). 즉,동등함 
EXPECT_TRUE(!(a < b) && !(b < a));

// 비록 값은 10으로 같으나 주소가 다른 영역에 있으므로 동등하지 않음
b = std::shared_ptr<int>{new int{10}};
EXPECT_TRUE(!(!(a < b) && !(b < a)));

// 별칭 생성자로 생성
// 소유권 개체 : a, 접근 대상 개체 : data
int data{10};
std::shared_ptr<int> c{a, &data};

// a, c 가 접근 대상 개체가 주소가 다른 영역에 있으므로 동등하지 않음
EXPECT_TRUE(!(!(a < c) && !(c < a)));

// a, c 의 소유권 개체는 동일하므로 동등함
EXPECT_TRUE(!(a.owner_before(c)) && !(c.owner_before(a)));

owner_less

owner_before()를 이용하여 소유권 개체의 주소로 비교하는 함수자입니다.

shared_ptr의 다형성

unique_ptr의 다형성의 경우와 마찬가지로 다형적 관계에 있는 경우 부모 클래스의 포인터로 변환됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
    virtual int Func() const {return 1;}
};
class Derived : public Base {
public:
    virtual int Func() const {return 2;}
};

std::shared_ptr<Base> base{new Base{}};
EXPECT_TRUE(base->Func() == 1);

std::shared_ptr<Derived> derived{new Derived{}};
EXPECT_TRUE(derived->Func() == 2);

base = derived; // shared_ptr<Derived>를 shared_ptr<Base>로 대입할 수 있습니다.

EXPECT_TRUE(base->Func() == 2);
EXPECT_TRUE(base.use_count() == 2 && derived.use_count() == 2);

// derived = base; // (X) 컴파일 오류. 반대는 안됩니다.

shared_ptr 형변환

shared_ptr 형변환 할때

1
dynamic_cast<std::shared_ptr<Base>>(derived);

를 사용하면, 내부 관리 개체를 형변환 하는 것이 아니라 shared_ptr자체를 형변환 하는 것이므로 동작하지 않습니다.

이를 위해 다음과 같은 형변환 함수들을 제공합니다.

항목 내용
const_pointer_cast() (C++11~) 상수성만 변환
static_pointer_cast() (C++11~) 타입 유사성을 지키며 변환
dynamic_pointer_cast() (C++11~) 타입 유사성을 지키며 변환.
Runtime Type Info(RTTI)가 있는 개체(가상 함수가 있는 개체)만 가능.
reinterpret_pointer_cast() (C++17~) 상속관계를 무시하고 변환.
정수를 포인터로 변환.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
    virtual ~Base() {}
    virtual int Func() {return 1;} // #1
};

class Derived : public Base {
public: 
    virtual int Func() override {return 2;} // #2
};

std::shared_ptr<Derived> derived{new Derived{}};

// dynamic_cast<std::shared_ptr<Base>>(derived); // (X) 컴파일 오류

// 관리하는 개체를 Derived에서 Base로 형변환합니다.
std::shared_ptr<Base> base{std::dynamic_pointer_cast<Base>(derived)};

// 가상 함수가 잘 호출됩니다.
EXPECT_TRUE(derived->Func() == 2 && base->Func() == 2);

// drived, base 총 2개
EXPECT_TRUE(derived.use_count() == 2 && base.use_count() == 2);

상호 참조

트리의 노드를 구현해 봅시다. 보통 Node는 부모로 이동할 수 있는 포인터와 자식들을 관리하는 컨테이너로 구성됩니다.

다음 코드에서는,

  1. shared_ptr로 부모 포인터를 관리합니다.
  2. vector<shared_ptr<Node>> 로 자식을 관리합니다.
  3. 생성자에서 부모 포인터를 전달받고, Add()로 자식을 추가합니다.

root를 생성하고, 자식 2개를 Add했으니 Node는 총 3개 생성됩니다. 그리고, root의 참조 카운트는 root 에서 1개, 자식 2개에서 m_Parent로 저장하고 있으니, 총 3개가 됩니다.

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
class Node {
    std::shared_ptr<Node> m_Parent; // 트리의 부모 노드
    std::vector<std::shared_ptr<Node>> m_Children; // 트리의 자식 노드

public:
    explicit Node(std::shared_ptr<Node> parent) : m_Parent(parent) {
        std::cout << "Node : Constructor" << std::endl;               
    } 
    ~Node() {
        std::cout << "Node : Destructor" << std::endl;    
    }
    void Add(std::shared_ptr<Node> child) {
        m_Children.push_back(child);
    }
};

// root는 부모가 없어서 nullptr로 노드를 생성합니다.
std::shared_ptr<Node> root{new Node{std::shared_ptr<Node>{}}};

// root를 부모 삼아 2개의 자식 노드를 생성합니다.
root->Add(std::shared_ptr<Node>{new Node{root}});
root->Add(std::shared_ptr<Node>{new Node{root}});

// root 1개 + child 2개 = 3개. root 개체가 소멸되면 참조 카운트만 2로 만드로 Node를 소멸시키지 않습니다.
EXPECT_TRUE(root.use_count() == 3);

실행 결과를 보면 다음과 같이 Node의 생성자만 불리고 소멸자가 안불립니다.

1
2
3
Node : Constructor
Node : Constructor
Node : Constructor

이는 root가 소멸할때 참조 카운트를 3에서 2로 바꿀뿐 관리하는 Node 소멸은 안하기 때문입니다. root에서 관리하는 Node소멸자가 안불리니 m_Children에 있는 자식 Node들도 소멸되지 않아 아무런 소멸자도 불리지 않게 됩니다. 자식 Nodem_Parent로 소유권을 갖고 있기 때문에 벌어진 일입니다.

이러한 경우를 상호 참조라 하며, 상호 참조시에 소유권을 공유하면, 서로 소유권을 주장하다가 결국 상기 예제처럼 아무것도 소멸을 못시키게 됩니다.

그래서 m_Parentshared_ptr이 아닌 소유권이 없는 일반 포인터로 만들 수도 있지만, weak_ptr로 해결하는게 예외에 좀더 안전합니다.

weak_ptr

weak_ptrshared_ptr로부터 생성되며, shared_ptr과 개체는 공유하지만, 소유권은 공유하지 않는 포인터 입니다. 즉, 참조 카운트를 증가/감소 하지 않고, 제어 블록에서 별도의 weak_ptr 카운트를 증가/감소 시키기만 할 뿐, 개체를 delete 하지 않습니다.(개체는 shared_ptr에서 delete합니다.)

상호 참조에서 m_Parentweak_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
class Node {
    std::weak_ptr<Node> m_Parent; // 트리의 부모 노드는 소유권이 없습니다.
    std::vector<std::shared_ptr<Node>> m_Children; // 트리의 자식 노드

public:
    explicit Node(std::weak_ptr<Node> parent) : m_Parent(parent) {
        std::cout << "Node : Constructor" << std::endl;               
    } 
    ~Node() {
        std::cout << "Node : Destructor" << std::endl;    
    }
    void Add(std::shared_ptr<Node> child) {
        m_Children.push_back(child);
    }
};

// root는 부모가 없어서 nullptr로 노드를 생성합니다.
std::shared_ptr<Node> root{new Node{std::weak_ptr<Node>{}}};

// root를 부모 삼아 2개의 자식 노드를 생성합니다.
root->Add(std::shared_ptr<Node>{new Node{root}});
root->Add(std::shared_ptr<Node>{new Node{root}});

// root 1개. weak_ptr 참조 카운트는 2개
EXPECT_TRUE(root.use_count() == 1);

root 의 참조 카운트는 1개여서 root소멸시 Node를 소멸시키며, vector가 소멸되고, vector에 추가됐던 자식 Node들이 소멸되어, 생성된 Node개체들이 모두 정상적으로 소멸됩니다.

1
2
3
4
5
6
Node : Constructor
Node : Constructor
Node : Constructor
Node : Destructor
Node : Destructor
Node : Destructor

weak_ptr 멤버 함수

항목 내용
constexpr weak_ptr() noexcept; (C++11~)

weak_ptr(const shared_ptr& other) noexcept; (C++11~)
shared_ptr로 부터 생성합니다.
weak_ptr(const weak_ptr& other) noexcept; (C++11~) 복사 생성합니다.
weak_ptr(const weak_ptr&& other) noexcept; (C++11~) 이동 생성합니다.
~weak_ptr(); (C++11~) 기존에 관리하던 개체를 해제합니다. shared_ptrweak_ptr 참조 카운트만 감소시킬 뿐 개체에 대한 delete를 수행하지는 않습니다.
weak_ptr& operator =(const weak_ptr& other) noexcept; (C++11~)

weak_ptr& operator =(const shared_ptr& other) noexcept; (C++11~)
weak_ptr이나 shared_ptr로 부터 복사 대입합니다.
weak_ptr& operator =(weak_ptr&& other) noexcept; (C++11~) 이동 대입합니다.
other가 관리하는 개체를 this로 이동시킵니다.
swap(unique_ptr& other) noexcept; (C++11~) 관리하는 개체를 other와 바꿔치기 합니다.
reset() noexcept; (C++11~) 기존에 관리하던 개체를 해제합니다. shared_ptrweak_ptr 참조 카운트만 감소시킬 뿐 개체에 대한 delete를 수행하지는 않습니다.
use_count() const noexcept; (C++11~) shared_ptr의 참조 카운트를 리턴합니다.
expired() const noexcept; (C++11~) 관리하는 개체가 소멸됐거나 소멸중인지 검사합니다. use_count() == 0과 같습니다.
lock() const noexcept; (C++11~) 관리하는 개체 접근을 위해 임시 shared_ptr을 생성합니다.
owner_before() const noexcept; (C++11~) 소유권 개체로 < 비교를 합니다.

weak_ptr 사용

weak_ptrshared_ptr이나 weak_ptr로 부터 생성됩니다. 관리하는 개체에 접근하려면, lock()함수를 이용하여 shared_ptr을 임시 생성하여 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::shared_ptr<int> sp1{new int{10}};
std::shared_ptr<int> sp2{sp1};
std::weak_ptr<int> wp1{sp1}; // shared_ptr로부터 생성됩니다.

EXPECT_TRUE(sp1.use_count() == 2 && sp1.use_count() == wp1.use_count()); // weak_ptr::use_count()는 shared_ptr의 참조 카운트 입니다.

std::weak_ptr<int> wp2{wp1}; // weak_ptr로부터 생성됩니다.

// *wp1 = 20; // weak_ptr은 관리하는 개체에 직접 접근할 수 없습니다.
std::shared_ptr<int> temp1{wp1.lock()}; // shared_ptr을 만들어 접근해야 합니다.
std::shared_ptr<int> temp2{wp2.lock()};

*temp1 = 20;
EXPECT_TRUE(*sp1 == 20 && *sp2 == 20 && *temp1 == 20 && *temp2 == 20);

EXPECT_TRUE(sp1.use_count() == 4); // sp1, sp2, temp1, temp2 총 4개 입니다.

bad_weak_ptr

shared_ptr에서 잘못된 weak_ptr을 사용할때 bad_weak_ptr 예외가 발생합니다.

다음 코드는 shared_ptr이 소멸된 후에 weak_ptr을 사용하다가 bad_weak_ptr 예외가 발생한 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
std::weak_ptr<int> wp;
{
    std::shared_ptr<int> sp{new int{10}};
    wp = sp;
} // sp가 소멸되었습니다.
try {
    // 소멸된 sp를 사용하는 wp로 shared_ptr을 만듭니다.
    // bad_weak_ptr 예외가 발생합니다.
    std::shared_ptr<int> error{wp};
}
catch (std::bad_weak_ptr&) {
    std::cout << "bad_weak_ptr" << std::endl;
}

(C++17~) 배열 지원

C++17 부터는 shared_ptr에서도 unique_ptr처럼 배열을 지원합니다.

1
2
std::shared_ptr<int[]> ptr{new int[3]{0, 1, 2}}; // 배열 개체. delete[] 를 호출하여 3개가 소멸됩니다.
EXPECT_TRUE(ptr[0] == 0 && ptr[1] == 1 && ptr[2] == 2);

(C++17~) weak_from_this

enable_shared_from_this의 멤버 함수 추가 (작성중)

(C++20~) make_shared()의 배열 지원

C++20 부터는 make_shared() 에서도 배열을 지원합니다.

1
2
std::shared_ptr<int[]> ptr{std::make_shared<int[]>(3)}; // 기본값으로 초기화된 3개의 int 요소를 가진 배열입니다.
EXPECT_TRUE(ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 0);

태그:

카테고리:

업데이트:

댓글남기기