C / C++,  Programming

[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를 생성 가능하다.

추가로 중간에 할당된 메모리를 해제하는 방법은 다음과 같다.

  1. std::unique_ptr::reset()
  2. std::unique_ptr::release()로 포인터를 받아와 수동으로 delete()
  3. 함수가 종료되면 자동적으로 delete

shared_ptr

shared_ptrunique_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

  1. http://www.tcpschool.com/cpp/cpp_template_smartPointer
  2. https://ks1171-park.tistory.com/6
  3. https://dydtjr1128.github.io/cpp/2019/05/10/Cpp-smart-pointer.html

Leave a Reply

Your email address will not be published. Required fields are marked *