[C++] Smart Pointer (unique_ptr / shared_ptr / weak_ptr)
기본적으로 C++ language는 garbage collector 기능을 지원하지 않기 때문에 memory dynamic allocation을 하면 deallocation 또한 프로그래머가 고려해줘야 한다. 이런 동작은 프로그램의 memory leak 문제를 발생시킬 수 있기 때문에 프로그램의 안정성을 낮춘다.
이를 위해서 smart pointer라는 기능이 C++11부터 제공된다.
일반적으로 new
키워드를 사용해 기본 포인터 (raw pointer)가 실제 메모리를 가리키도록 초기화가 되는데, 이 주소를 smart pointer 생성자에 대입하게 되면 smart pointer object가 생성된다. 그렇게 되면 따로 memory deallocation을 해줄 필요가 없어진다.
C++11 이전에는 auto_ptr
이라는 키워드를 통해 smart pointer 작업을 진행해왔다. 그 후엔 <memory> header file에 정의된 unique_ptr
, shared_ptr
, weak_ptr
을 통해서 사용 가능하다.
Memory Leak Example
#include <iostream> #include <memory> #include <vector> using namespace std; class A { public: A() { cout << "A constructor" << endl; } ~A() { cout << "A destructor" << endl; } }; int main() { vector<A *> list; list.resize(3); for (int i=0; i<3; i++) { list[i] = new A(); } return 0; }
A constructor
A constructor
A constructor
위 예제는 memory leak이 존재하는 예시다. vector.resize()
함수는 내부적으로 동적할당을 해 heap memory를 사용하게 되는데, 뒤에 new
를 통해 다시 동적할당을 진행하면서 기존에 할당한 memory allocation 주소를 잃어버리게 된다.
$ valgrind ./a.out ==189603== Memcheck, a memory error detector ==189603== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==189603== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==189603== Command: ./a.out ==189603== A constructor A constructor A constructor ==189603== ==189603== HEAP SUMMARY: ==189603== in use at exit: 3 bytes in 3 blocks ==189603== total heap usage: 4 allocs, 1 frees, 27 bytes allocated ==189603== ==189603== LEAK SUMMARY: ==189603== definitely lost: 3 bytes in 3 blocks ==189603== indirectly lost: 0 bytes in 0 blocks ==189603== possibly lost: 0 bytes in 0 blocks ==189603== still reachable: 0 bytes in 0 blocks ==189603== suppressed: 0 bytes in 0 blocks ==189603== Rerun with --leak-check=full to see details of leaked memory ==189603== ==189603== For lists of detected and suppressed errors, rerun with: -s ==189603== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
valgrind를 통해 memory error를 확인해보면 definitely lost: 3 bytes in 3 blocks
가 발생된 것을 볼 수 있다.
unique_ptr
unique_ptr
은 오직 하나의 smart pointer만이 특정 object를 소유할 수 있도록 한다. 따라서 주소 복사를 통해 두 개 이상의 object가 존재할 수 없기 때문에 오직 이동 동작만 가능하다.
#include <iostream> #include <memory> #include <vector> using namespace std; class A { public: A(int value) { cout << "A constructor: " << value << endl; _value = value; } ~A() { cout << "A destructor: " << _value << endl; } int _value; }; int main() { unique_ptr<A> a(new A(10)); // unique_ptr<A> b = a; unique_ptr<A> b = move(a); return 0; }
A constructor: 10
A destructor: 10
위 코드를 보면, class A
의 instance a
가 생성되고, b
로 곧바로 assignment가 되지 않고 move()
를 이용해야 가능한 것을 볼 수 있다. 따라서 메모리 할당과 해제가 unique_ptr
에 의해 자동으로 수행되는 것을 출력되는 결과를 통해 볼 수 있다.
// make_uniqe<Type>(constructor arguments) unique_ptr<myClass> inst = make_unique<myClass>(arg0, arg1);
#include <iostream> #include <memory> using namespace std; class A { public: A(int value) { cout << "A constructor: " << value << endl; _value = value; } ~A() { cout << "A destructor: " << _value << endl; } int _value; }; int main() { unique_ptr<A> a(new A(10)); unique_ptr<A> b = make_unique<A>(20); return 0; }
A constructor: 10
A constructor: 20
A destructor: 20
A destructor: 10
또한 C++14 이후부터 제공되는 make_unique()
함수를 통해 unique_ptr
object를 생성 가능하다.
추가로 중간에 할당된 메모리를 해제하는 방법은 다음과 같다.
std::unique_ptr::reset()
std::unique_ptr::release()
로 포인터를 받아와 수동으로delete()
- 함수가 종료되면 자동적으로 delete
shared_ptr
shared_ptr
은 unique_ptr
과 다르게 여러개의 변수에서 referencing이 가능하며, 하나의 특정 object를 참조하는 smart pointer가 몇개인지를 참조 가능한 smart pointer다. 물론 shared_ptr
또한 reference counter가 0이 되면 자동으로 메모리를 해제한다.
#include <iostream> #include <memory> using namespace std; int main() { shared_ptr<int> a(new int); *a = 10; auto b(a); *b = 20; auto c = a; *c = 30; cout << "*a=" << *a << endl; cout << "*b=" << *b << endl; cout << "*c=" << *c << endl; cout << "Reference counter = " << a.use_count() << endl; return 0; }
*a=30 *b=30 *c=30 Reference counter = 3
shared_ptr
또한 C++14 이후부터 제공되는 make_unique()
함수를 통해 unique_ptr
object를 생성 가능하다.
// make_shared<Type>(constructor arguments) shared_ptr<myClass> inst = make_shared<myClass>(arg0, arg1);
참고로, 처음에 언급한 memory leak문제가 된다는 코드를 shared_ptr을 사용해 고쳐진 코드는 다음과 같다.
#include <iostream> #include <memory> using namespace std; class A { public: A(): value(0) { cout << "A constructor" << endl; } ~A() { cout << "A destructor" << endl; } int value; }; int main() { shared_ptr<A[]> list; list.reset(new A[3]()); for (int i=0; i<3; i++) { list[i].value = i; } for (int i=0; i<3; i++) { cout << list[i].value << endl; } return 0; }
weak_ptr
하나 이상의 shared_ptr
instance가 소유하는 object에 대한 접근을 제공하지만 소유자의 수에는 포함되지 않는 smart pointer다. 만약 서로가 상대방을 가리키는 shared_ptr
을 가지고 있다면 reference counter는 영원히 0이 되지 않기 때문에 memory leak 문제가 발생될 수 있다.
따라서 circular reference 문제를 해결하기 위해 weak_ptr
을 써서 제거 가능하다.
#include <iostream> #include <memory> using namespace std; struct Node { int data; shared_ptr<Node> next; Node() { cout << "Node()" << endl; } ~Node() { cout << "Deconstruction" << endl; } }; int main() { shared_ptr<Node> a(new Node); shared_ptr<Node> b(new Node); cout << "a ref count = " << a.use_count() << endl; cout << "b ref count = " << b.use_count() << endl; a->next = b; b->next = a; cout << "----------------------------" << endl; cout << "a ref count = " << a.use_count() << endl; cout << "b ref count = " << b.use_count() << endl; return 0; }
Node() Node() a ref count = 1 b ref count = 1 a ref count = 2 b ref count = 2
위와 같은 memory leak 문제를 해결하기 위해 weak_ptr
을 사용하면 된다.
#include <iostream> #include <memory> using namespace std; struct Node { int data; weak_ptr<Node> next; Node() { cout << "Node()" << endl; } ~Node() { cout << "Deconstruction" << endl; } }; int main() { shared_ptr<Node> a(new Node); shared_ptr<Node> b(new Node); cout << "a ref count = " << a.use_count() << endl; cout << "b ref count = " << b.use_count() << endl; a->next = b; b->next = a; cout << "----------------------------" << endl; cout << "a ref count = " << a.use_count() << endl; cout << "b ref count = " << b.use_count() << endl; return 0; }
Node() Node() a ref count = 1 b ref count = 1 a ref count = 1 b ref count = 1 Deconstruction Deconstruction
Reference
- http://www.tcpschool.com/cpp/cpp_template_smartPointer
- https://ks1171-park.tistory.com/6
- https://dydtjr1128.github.io/cpp/2019/05/10/Cpp-smart-pointer.html