스마트포인터

스마트포인터

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

deleter

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

template<typename T> 
struct default_delete<T[]>
{
void operator()(T* ptr) const
    {
        static_assert(sizeof(T)>0, "can't delete pointer to incomplete type");
        delete [] ptr;
    }
};

auto deleter = default_delete<int>();
int* i = new int();
deleter(i);

비교연산자

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

inline bool operator!=(const unique_ptr<_Tp, _Tp_Deleter>& __x,
                       const unique_ptr<_Up, _Up_Deleter>& __y)
{ return !(__x.get() == __y.get()); }

inline bool operator<(const unique_ptr<_Tp, _Tp_Deleter>& __x,
                      const unique_ptr<_Up, _Up_Deleter>& __y)
{ return __x.get() < __y.get(); }

포인터연산자 *, ->

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

T& operator* const {
    _GLIBCXX_DEBUG_ASSERT(*(this->pointer) != nullptr);
    return *(this->pointer);
}

T* operator->() const {
    _GLIBCXX_DEBUG_ASSERT(this->pointer != nullptr);
    return this->pointer;
}

unique_ptr

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

사용방법

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

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

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

// c++11
unique_ptr<vector> pvec(new vector(10, 30));
unique_ptr<vector> pvec2(move(vec));          // 이동생성자 호출. pvec에는 nullptr 이 저장됨.

// 사실 여기서 해주는 작업이 c++11과 같은데, c++14에서 표준으로 도입됐지만 c++11에도 구현되어있다.
unique_ptr<int> pi = make_unique<int>();
*pi = 20;
unique_ptr<int> pi2 = move(pi);

구현코드 예시

template <typename T, typename T_Deleter = default_delete<T>>
class unique_ptr
{
public:
    T*        pointer;
    T_Deleter deleter;

public:
    unique_ptr() : pointer(nullptr), deleter(T_Deleter()) {}
    unique_ptr(T* ptr) : pointer(ptr), deleter(T_Deleter()) {}

    // 소멸자에서 포인터 삭제
    ~unique_ptr()
    {
        if (pointer != nullptr) deleter(pointer);
    }

    // 복사생성자 삭제 (이동만 가능하도록)
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    // 이동생성자. 이동할땐 기존 unique_ptr은 소유권을 포기
    unique_ptr(unique_ptr&& rhs)
    {
        this->pointer = rhs.pointer;
        this->deleter = rhs.deleter;
        rhs.pointer = nullptr;
    }
    unique_ptr& operator= (unique_ptr&& rhs)
    {
        this->pointer = rhs.pointer;
        this->deleter = rhs.deleter;
        rhs.pointer = nullptr;
        return *this;
    }

    // 포인터를 반환하고 소유권을 해제한다. 
    T* release() {
        T* ret = this->pointer;
        this->pointer = nullptr;
        return ret;
    }

};

template <class T, class... Args>
unique_ptr<T> make_unique(Args... args)
{
    return unique_ptr<T>(new T(args...));
}

shared_ptr

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

구현코드예시

template <typename T, typename T_Deleter = default_delete<T>>
class shared_ptr {
private:
    T_Deleter deleter;
    T*        pointer;
    size_t*   counter;        // 사실 shared_count 뿐만아니라 weak_count도 같이 저장해야한다.

public:
    // 기본 생성자
    shared_ptr() : pointer(nullptr), counter(nullptr) {}

    // 생성자
    explicit shared_ptr(T* p) : pointer(p), counter(new size_t(1)) {}

    // 소멸자
    ~shared_ptr() {
        release();
    }

    // 복사 생성자. 포인터를 복사하면서 counter를 증가시키고, counter 포인터도 공유한다. 
    shared_ptr(const shared_ptr& other) : pointer(other.pointer), counter(other.counter) {
        if(counter) {
            ++(*counter);
        }
    }

    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            release();
            pointer = other.pointer;
            counter = other.counter;
            if(counter) {
                ++(*counter);
            }
        }
        return *this;
    }

    // 이동 생성자. 이동이라 참조카운트 증가하지 않음
    shared_ptr(shared_ptr&& other) : pointer(other.pointer), counter(other.counter) {
        other.pointer = nullptr;
        other.counter = nullptr;
    }

    shared_ptr& operator=(shared_ptr&& other) {
        if (this != &other) {
            release();
            pointer = other.pointer;
            counter = other.counter;
            other.pointer = nullptr;
            other.counter = nullptr;
        }
        return *this;
    }

    // shared_ptr의 리소스 해제. 참조카운트가 0이면 원시포인터의 객체도 소멸시켜준다.
    void release() {
        if (counter && --(*counter) == 0) {
            deleter(pointer);
            delete counter;
        }
        pointer = nullptr;
        counter = nullptr;
    }
};

문제점

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

class Knight
{
...
    shared_ptr<Knight> _target = nullptr;  // 얘도 객체라 Knight와 수명이 같다.
}

int main()
{
    shared_ptr<Knight> k1 = make_shared<Knight>();
    shared_ptr<Knight> k2 = make_shared<Knight>();
    k1->_target = k2;
    k2->_target = k1;
    // k1->_target = nullptr;       // 순환구조 끊기
}

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을 하나 생성해서 잠근다.

class Knight
{
...
    void Attack() {
        // if (_target.expired() == false)   // 객체의 존재여부 확인

        // 사용중에 객체가 사라지지 않게 shared_ptr 객체를 생성해서 잠근다.
        // 이 sptr 객체의 수명은 Attack 메서드 내부이다 
        // 없다면 nullptr이 반환되어 else 구문이 실행된다. 
        shared_ptr<Knight> sptr = _target.lock();
        if (sptr)
            // object exist
        else
            // not exist
    }
    weak_ptr<Knight> _target;     // weak_ptr은 nullptr로 초기화할 수 없다.
}
int main()
{
    shared_ptr<Knight> k1 = make_shared<Knight>();

    {
        // k2 객체의 수명은 블록 내부이다. 
        shared_ptr<Knight> k2 = make_shared<Knight>();
        k1->_target = k2;         // k2의 refcount 는 증가하지 않는다.
    }  
    // k2 객체가 소멸되면서 refcount가 0이되고, k2가 가리키던 Knight도 소멸된다.

    k1->Attack();  // _target이 실제로 존재하는 객체인지에 따라 코드 진행
}

구현코드예시

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

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

struct ControlBlock {
    size_t shared_counter;
    size_t weak_counter;

    ControlBlock() : shared_counter(1), weak_counter(0) {}
    ~ControlBlock() { }
};

template<typename T>
class weak_ptr {
private:
    T*            pointer;
    ControlBlock* control_block;

public:
    weak_ptr() : pointer(nullptr), control_block(nullptr) { }

    weak_ptr(const shared_ptr<T>& sptr)
    : control_block(sptr->getControlBlock()) {
        ++control_block->weak_counter;
    }

    // weak_counter, shared_counter 모두 0일때 control_block 을 지워준다. 
    ~weak_ptr() {
        if (control_block) {
            --control_block->weak_counter;
            if (control_block->shared_counter == 0 && control_block->weak_counter == 0) {
                delete control_block;
            }
        }
    }

    size_t use_count() const
    { return control_block->shared_counter; }

    bool expired() const
    { return use_count == 0; }

    shared_ptr<T> lock() {
        if (control_block && control_block->shared_counter > 0) {
            return shared_ptr<T>(*this);
        }
        return nullptr;
    }
};

auto_ptr

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

Comments

ESC
Type to search...