스마트포인터
스마트포인터
포인터처럼 동작하는 클래스 템플릿이며, 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 = #
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