[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
