스마트포인터

스마트포인터

2023년 7월 9일
c++11

스마트포인터 #

포인터처럼 동작하는 클래스 템플릿이며, c++11부터 표준에 포함되어 <memory> 헤더에 정의되어있다.
RAII패턴으로, 지역변수처럼 선언해서 동작하기 때문에 스코프를 벗어나며 클래스의 소멸자가 호출될때 가리키고있던 원시포인터가 사용되지 않을경우 자동으로 할당한 메모리도 해제해줘서 메모리 leak(댕글링포인터)에서 안전하게 사용할 수 있다.
예외 발생시에도 소멸자를 호출해줘서 자동으로 메모리를 해제해준다.

deleter #

포인터를 지울때 deleter를 호출해서 지운다.

 1template<typename T> 
 2struct default_delete<T[]>
 3{
 4void operator()(T* ptr) const
 5    {
 6        static_assert(sizeof(T)>0, "can't delete pointer to incomplete type");
 7        delete [] ptr;
 8    }
 9};
10
11auto deleter = default_delete<int>();
12int* i = new int();
13deleter(i);

비교연산자 #

shared_ptr, unique_ptr에 구현되어 있다. 직접 원시 포인터에 접근해서 포인터 비교연산을 수행한다.

1inline bool operator!=(const unique_ptr<_Tp, _Tp_Deleter>& __x,
2                       const unique_ptr<_Up, _Up_Deleter>& __y)
3{ return !(__x.get() == __y.get()); }
4
5inline bool operator<(const unique_ptr<_Tp, _Tp_Deleter>& __x,
6                      const unique_ptr<_Up, _Up_Deleter>& __y)
7{ return __x.get() < __y.get(); }

포인터연산자 *, -> #

이것도 포인터 연산과 동일하게 사용할 수 있도록 구현되어 있다. -> 연산자의 구현이 원시포인터를 그대로 반환하는 이유는 원시포인터 반환 후 -> 연산자가 그대로 남아 원시포인터의 -> 연산을 수행하기 때문이다.

1T& operator* const {
2    _GLIBCXX_DEBUG_ASSERT(*(this->pointer) != nullptr);
3    return *(this->pointer);
4}
5
6T* operator->() const {
7    _GLIBCXX_DEBUG_ASSERT(this->pointer != nullptr);
8    return this->pointer;
9}

unique_ptr #

객체를 가리키는 포인터를 단 하나만 선언할 수 있도록 관리해준다. 복사는 금지되며, 소유권이전으로 이동 연산만 가능하다.
deleter가 빈객체(0 or 1 byte)이기 때문에 포인터 저장공간만 필요하므로 리소스 낭비 없이 사용할 수 있다.

사용방법 #

unique_ptr는 원시포인터를 더이상 사용하지 않으면서 유일함이 보장된다.

1// 잘못사용하는 unique_ptr
2int num = 10;
3int* a = &num;
4unique_ptr<int> u_ptr(a);
5unique_ptr<int> u_ptr2(move(u_ptr));  // (o) move로 이동생성자 호출해서 u_ptr은 nullptr이 저장됨.
6unique_ptr<int> u_ptr3(a);            // (x) a를 가리키는 unique_ptr이 2개가 되지만, 에러는 발생하지 않음.

unique_ptr을 생성할때 new 생성자로 포인터를 바로 넣거나, c++14 부터 사용가능한 make_unique를 사용한다.

1// c++11
2unique_ptr<vector> pvec(new vector(10, 30));
3unique_ptr<vector> pvec2(move(vec));          // 이동생성자 호출. pvec에는 nullptr 이 저장됨.
4
5// 사실 여기서 해주는 작업이 c++11과 같은데, c++14에서 표준으로 도입됐지만 c++11에도 구현되어있다.
6unique_ptr<int> pi = make_unique<int>();
7*pi = 20;
8unique_ptr<int> pi2 = move(pi);

구현코드 예시 #

 1template <typename T, typename T_Deleter = default_delete<T>>
 2class unique_ptr
 3{
 4public:
 5    T*        pointer;
 6    T_Deleter deleter;
 7
 8public:
 9    unique_ptr() : pointer(nullptr), deleter(T_Deleter()) {}
10    unique_ptr(T* ptr) : pointer(ptr), deleter(T_Deleter()) {}
11
12    // 소멸자에서 포인터 삭제
13    ~unique_ptr()
14    {
15        if (pointer != nullptr) deleter(pointer);
16    }
17
18    // 복사생성자 삭제 (이동만 가능하도록)
19    unique_ptr(const unique_ptr&) = delete;
20    unique_ptr& operator=(const unique_ptr&) = delete;
21
22    // 이동생성자. 이동할땐 기존 unique_ptr은 소유권을 포기
23    unique_ptr(unique_ptr&& rhs)
24    {
25        this->pointer = rhs.pointer;
26        this->deleter = rhs.deleter;
27        rhs.pointer = nullptr;
28    }
29    unique_ptr& operator= (unique_ptr&& rhs)
30    {
31        this->pointer = rhs.pointer;
32        this->deleter = rhs.deleter;
33        rhs.pointer = nullptr;
34        return *this;
35    }
36
37    // 포인터를 반환하고 소유권을 해제한다. 
38    T* release() {
39        T* ret = this->pointer;
40        this->pointer = nullptr;
41        return ret;
42    }
43
44};
45
46template <class T, class... Args>
47unique_ptr<T> make_unique(Args... args)
48{
49    return unique_ptr<T>(new T(args...));
50}

shared_ptr #

한 객체에 대해 여러개의 포인터가 필요할때 원시포인터에 대한 참조카운트 정보를 가지고 모든 shared_ptr이 사라질때 원시포인터도 delete해준다.
unique_ptr과 마찬가지로 참조카운트를 제대로 관리하려면 shared_ptr를 원시포인터로 초기화 후 이후엔 shared_ptr 객체를 이용해서 복사, 이동해야한다.

구현코드예시 #

 1template <typename T, typename T_Deleter = default_delete<T>>
 2class shared_ptr {
 3private:
 4    T_Deleter deleter;
 5    T*        pointer;
 6    size_t*   counter;        // 사실 shared_count 뿐만아니라 weak_count도 같이 저장해야한다.
 7
 8public:
 9    // 기본 생성자
10    shared_ptr() : pointer(nullptr), counter(nullptr) {}
11
12    // 생성자
13    explicit shared_ptr(T* p) : pointer(p), counter(new size_t(1)) {}
14
15    // 소멸자
16    ~shared_ptr() {
17        release();
18    }
19
20    // 복사 생성자. 포인터를 복사하면서 counter를 증가시키고, counter 포인터도 공유한다. 
21    shared_ptr(const shared_ptr& other) : pointer(other.pointer), counter(other.counter) {
22        if(counter) {
23            ++(*counter);
24        }
25    }
26
27    shared_ptr& operator=(const shared_ptr& other) {
28        if (this != &other) {
29            release();
30            pointer = other.pointer;
31            counter = other.counter;
32            if(counter) {
33                ++(*counter);
34            }
35        }
36        return *this;
37    }
38
39    // 이동 생성자. 이동이라 참조카운트 증가하지 않음
40    shared_ptr(shared_ptr&& other) : pointer(other.pointer), counter(other.counter) {
41        other.pointer = nullptr;
42        other.counter = nullptr;
43    }
44
45    shared_ptr& operator=(shared_ptr&& other) {
46        if (this != &other) {
47            release();
48            pointer = other.pointer;
49            counter = other.counter;
50            other.pointer = nullptr;
51            other.counter = nullptr;
52        }
53        return *this;
54    }
55
56    // shared_ptr의 리소스 해제. 참조카운트가 0이면 원시포인터의 객체도 소멸시켜준다.
57    void release() {
58        if (counter && --(*counter) == 0) {
59            deleter(pointer);
60            delete counter;
61        }
62        pointer = nullptr;
63        counter = nullptr;
64    }
65};

문제점 #

순환구조에서 서로를 참조해 순환구조를 끊어주기 전까지는 메모리릭이 발생한다.

 1class Knight
 2{
 3...
 4    shared_ptr<Knight> _target = nullptr;  // 얘도 객체라 Knight와 수명이 같다.
 5}
 6
 7int main()
 8{
 9    shared_ptr<Knight> k1 = make_shared<Knight>();
10    shared_ptr<Knight> k2 = make_shared<Knight>();
11    k1->_target = k2;
12    k2->_target = k1;
13    // k1->_target = nullptr;       // 순환구조 끊기
14}

main이 종료되며 k1이 소멸하며 refcnt는 1이 되어 k1이 가리키던 객체는 남아있게된다.
k1->_target이 남아있어 k2가 소멸돼도 refcnt가 1이되어 k2의 객체는 남아있게된다.
서로를 참조하는 _target 객체가 소멸되지 않았기 때문에 두 객체 모두 메모리에 남게된다.


weak_ptr #

shared_ptr의 순환참조 문제를 해결하기 위해 만들어졌는데, 참조카운트를 증가시키지 않는 포인터이다.
expired() 메서드로 참조카운트가 존재하는지(true/false) 확인할 수 있고, use_count() 메서드로 참조카운트 숫자를 알 수 있다.
weak_ptr을 사용하는 중에 원본객체가 사라지지 않도록 lock() 으로 shared_ptr을 하나 생성해서 잠근다.

 1class Knight
 2{
 3...
 4    void Attack() {
 5        // if (_target.expired() == false)   // 객체의 존재여부 확인
 6
 7        // 사용중에 객체가 사라지지 않게 shared_ptr 객체를 생성해서 잠근다.
 8        // 이 sptr 객체의 수명은 Attack 메서드 내부이다 
 9        // 없다면 nullptr이 반환되어 else 구문이 실행된다. 
10        shared_ptr<Knight> sptr = _target.lock();
11        if (sptr)
12            // object exist
13        else
14            // not exist
15    }
16    weak_ptr<Knight> _target;     // weak_ptr은 nullptr로 초기화할 수 없다.
17}
18int main()
19{
20    shared_ptr<Knight> k1 = make_shared<Knight>();
21
22    {
23        // k2 객체의 수명은 블록 내부이다. 
24        shared_ptr<Knight> k2 = make_shared<Knight>();
25        k1->_target = k2;         // k2의 refcount 는 증가하지 않는다.
26    }  
27    // k2 객체가 소멸되면서 refcount가 0이되고, k2가 가리키던 Knight도 소멸된다.
28
29    k1->Attack();  // _target이 실제로 존재하는 객체인지에 따라 코드 진행
30}

구현코드예시 #

사실 위에서 구현한 shared_ptr의 참조카운트블록은 size_t* 형으로 구현되어있는데, 잘못구현된 코드이다.
weak_ptr도 참조 추적을 위해 shared_ptr과 동일한 참조카운트블록을 가리켜야하는데, shared_ptr이 삭제됐다고 블록의 메모리를 해제하면 같은 객체를 가리키던 weak_ptr에서 참조카운트가 0이됐는지 확인할때 해제된 공간에 접근하게된다.

실제 구현코드에서는 참조카운트블록이 객체 포인터 관리를 위한 shared_counter, 참조카운트블록 포인터 관리를 위한 weak_counter 를 함께 관리하게된다.

 1struct ControlBlock {
 2    size_t shared_counter;
 3    size_t weak_counter;
 4
 5    ControlBlock() : shared_counter(1), weak_counter(0) {}
 6    ~ControlBlock() { }
 7};
 8
 9template<typename T>
10class weak_ptr {
11private:
12    T*            pointer;
13    ControlBlock* control_block;
14
15public:
16    weak_ptr() : pointer(nullptr), control_block(nullptr) { }
17
18    weak_ptr(const shared_ptr<T>& sptr)
19    : control_block(sptr->getControlBlock()) {
20        ++control_block->weak_counter;
21    }
22
23    // weak_counter, shared_counter 모두 0일때 control_block 을 지워준다. 
24    ~weak_ptr() {
25        if (control_block) {
26            --control_block->weak_counter;
27            if (control_block->shared_counter == 0 && control_block->weak_counter == 0) {
28                delete control_block;
29            }
30        }
31    }
32
33    size_t use_count() const
34    { return control_block->shared_counter; }
35
36    bool expired() const
37    { return use_count == 0; }
38
39    shared_ptr<T> lock() {
40        if (control_block && control_block->shared_counter > 0) {
41            return shared_ptr<T>(*this);
42        }
43        return nullptr;
44    }
45};

auto_ptr #

c++11부터 사용되지 않고, c++17에서 제거된 스마트포인터.

comments powered by Disqus