스마트포인터
2023년 7월 9일
스마트포인터 #
포인터처럼 동작하는 클래스 템플릿이며, 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 = #
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에서 제거된 스마트포인터.