About Smart Pointer in C++
Jul 18
Programming/C/C plus plus smart pointer No Comments
Smart Pointer in C++
최규식
나는 C언어보다 JAVA를 먼저 배웠다. 그래서 C를 처음 접했을 때 메모리 유출을 막기 위해서 프로그램을 짜면서 신경써 줘야 하는 것들에 대해서 매우 짜증을 냈다. 특히 C++은 엄청난 고수준 언어임에도 불구하고 메모리 관리에 대해서는 저수준이나 다름 없는 면을 보여 주고 있다. 나는 C++의 추상화를 통한 화려하고 막강한 기능을 보면서 감탄하면서도 한편으로는 도데체 메모리 내에 어떤 과정을 거치고 있는 것일까를 항상 염려한다.
프로그램에서 쓰이는 자료구조가 복잡해지면 동적으로 할당받아야 하는 메모리도 많아지게 되고 그에 따라 메모리의 유출의 가능성이 높아지게 된다. 메모리 유출을 막고 안정성을 높이기 위해서 대다수의 자료구조들은 입력으로 받은 자료들을 복사해서 자료구조 내에 저장해 두는 방식을 많이 채택한다. 예를 들어 vector클래스에 string object를 저장할 때 string object의 pointer를 가지고 있는 것이 훨씬 효율적임에도 불구하고 안정성을 위해서 string object자체의 복사본을 만들어서 저장해 둔다. 그렇게 함으로써 vector object가 destruct될 때 vector가 가지고 있던 strign object들을 안전하게 destruct할 수 있다. 그리고 vector를 사용하는 입장에서도 이미 복사본이 만들어졌으므로 안심하고 원본에 해당하는 string object를 destruct할 수 있다.
그러나 이런 방식은 안정성과 편리함을 실현할 수 있지만, 많은 메모리 복사때문에 효율성이 떨어진다. 반면에 복사본을 만들지 않고 그것의 pointer만을 저장하는 경우에는 vector사용자가 입력으로 넣어준 string object의 destruct시기를 알기 위해서 vector가 언제까지 쓰이는지 지켜봐야하기 때문에 실수가 많아지고 메모리 유출, 혹은 이미 destruct된 object를 또 destruct하려고 하는(유닉스에서는 대부분 이런 경우에 segmentation fault가 발생한다.)일이 발생하게 된다. 그러나 실수만 없다면 확실한 효율성을 보장 받을 수 있다.
반면에 자바에서는 이 문제를 쓰레기 수집(garbage collection)이라는 방식으로 해결한다. 복사를 하지 않고 포인터를 사용하여 자료의 삽입 삭제를 하지만 메모리 유출을 막기 위해서 레퍼런스를 잃은 object를 자동으로 찾아 다니며 free시켜 주는 방식이다. 이 방법은 복사로 인한 오버헤드가 적고 프로그래머가 메모리에 대해 신경 써 줘야할 부분이 적기 때문에 위의 두 방식의 장점을 취한 것이라고 볼 수는 있지만 결정적으로 garbage collector를 실행하기 위한 오버헤드가 커져서 또 trade off가 생기게 된다. 따라서 자바에서는 garbage collector가 성능을 좌우하게 된다.
나는 C++에서 자바와 비슷한 자료구조 방식을 가지면서(복사본을 만들지 않음) garbage collector를 사용하지도 않으면서 메모리 유출을 막고 프로그래머가 신경을 덜 쓰게 할 수 있을 것이라고 생각을 하고 다음과 같은 smart_pointer라는 class를 만들었다.
#define HAVE_STRING_H 1#ifdef HAVE_STRING_H#include <string>#endiftemplate <class T>class smart_pointer {private: unsigned int* _count_p; T* _refered_object;public: inline smart_pointer(void);#ifdef HAVE_STRING_H inline smart_pointer(const char* str);#endif inline smart_pointer(const T* x); inline smart_pointer(const smart_pointer& x); inline ~smart_pointer(void); inline smart_pointer& operator=(int n); smart_pointer& operator=(T* x); smart_pointer& operator=(smart_pointer& x);#ifdef HAVE_STRING_H smart_pointer<string>& operator=(const char* str);#endif inline T& operator*() const; inline T* operator->() const; inline bool operator==(const smart_pointer& x) const; inline bool operator<(const smart_pointer& x) const; inline bool operator>(const smart_pointer& x) const; inline int ref_count() const;};template <class T>smart_pointer<T>::smart_pointer(void) :_count_p(0), _refered_object(0) {}#ifdef HAVE_STRING_Hsmart_pointer<string>::smart_pointer(const char *str) :_count_p(new unsigned int(1)), _refered_object(new string(str)) {}#endiftemplate <class T>smart_pointer<T>::smart_pointer(const T* x) :_count_p(new unsigned int(1)), _refered_object((T*) x) {}template <class T>smart_pointer<T>::smart_pointer(const smart_pointer<T>& x) :_count_p(x._count_p), _refered_object(x._refered_object){ if (_refered_object) ++*_count_p;}template <class T>smart_pointer<T>::~smart_pointer(void){ if (_refered_object && !--*_count_p) { delete _refered_object; delete _count_p; }}template <class T>smart_pointer<T>& smart_pointer<T>::operator=(int n){ if (_refered_object && !--*_count_p) { delete _refered_object; delete _count_p; } _refered_object = 0; _count_p = 0;}#ifdef HAVE_STRING_Hsmart_pointer<string>& smart_pointer<string>::operator=(const char* str){ if (_refered_object) { if (!--*_count_p) { delete _refered_object; } else { _count_p = new unsigned int; } } if (!_count_p) { _count_p = new unsigned int; } _refered_object = new string(str); *_count_p = 1; return *this;}#endiftemplate <class T>smart_pointer<T>& smart_pointer<T>::operator=(T* x){ if (_refered_object) { if (!--*_count_p) { delete _refered_object; } else { _count_p = new unsigned int; } } if (!_count_p) { _count_p = new unsigned int; } _refered_object = x; *_count_p = 1; return *this;}template <class T>smart_pointer<T>& smart_pointer<T>::operator=(smart_pointer<T>& x){ if (this == &x) return *this; if (_refered_object && !--*_count_p) { delete _refered_object; delete _count_p; } _refered_object = x._refered_object; ++*(_count_p = x._count_p); return *this;}template <class T>T& smart_pointer<T>::operator*() const{ return *_refered_object;}template <class T>T* smart_pointer<T>::operator->() const{ return _refered_object;}template <class T>bool smart_pointer<T>::operator==(const smart_pointer<T>& x) const{ return *_refered_object == *x._refered_object;}template <class T>bool smart_pointer<T>::operator<(const smart_pointer<T>& x) const{ return *_refered_object < *x._refered_object;}template <class T>bool smart_pointer<T>::operator>(const smart_pointer<T>& x) const{ return *_refered_object > *x._refered_object;}template <class T>int smart_pointer<T>::ref_count() const{ return *_count_p;}
smart_pointer는 외부에서 볼 때는 pointer와 똑같은 일을 하면서 point되는 object가 레퍼런스를 잃으면 자동으로 delete를 해 주는 포인터이다. 따라서 프로그래머는 new operator를 이용해서 object를 생성하고 그 포인터를 smart_pointer에 저장해 두면 그 다음에 그 object가 쓰는 메모리에 관해서는 신경을 쓰지 않아도 된다.
기본적인 아이디어는 assign등이나 initialize등을 할 때 어떤 object에 대한 reference count를 세게하는 것이다. 그래서 그 reference count가 0되면 비로소 smart_pointer의 destructor에서 object의 destructor를 불러 주게 된다. 따라서 그 object를 하나 이상의 smart_pointer가 reference를 하고 있다면 그 object는 사라지지 않는다.
smart_pointer를 사용할 때는 마치 JAVA의 레퍼런스 타입의 변수를 사용하듯이 하면 된다. 함수 안에서 사용한 smart_pointer는 함수가 끝나고 그 scope에서 사라질 때 그것이 가리키던 object를 자동으로 destruct시킨다. 다음은 그 예제이다.
#include ``smart_pointer.h''#include <string>typedef smart_pointer<string> String;void f(void){ String a = new string(``hello''); // 혹은 String a = ``hello''도 됨. String b = ``world'';} //a와 b가 레퍼런스하던 것이 자동으로 destruct된다.
그리고 smart_pointer가 다른 것을 가리키게 해도 레퍼런스를 잃은(원래 가리키고 있던) object를 자동으로 destruct한다.
void f(void){ String a = ``hello''; String b = ``hi''; b = a; //b가 레퍼런스하던 것이 자동으로 destruct된다.}
기존의 object를 가리키게 초기화할 수도 있다.
void f(void){ String a = ``hello''; String b = a; // b를 a와 같은 것을 가리키도록 초기화. a = ``new string''; // ``hello''를 이미 b가 레퍼런스하고 있으므로 ``hello''가 destruct // 되지는 않는다.}
이정도의 예제만 봐도 충분히 자바의 레퍼런스와 상당히 쓰임새가 비슷하다는 것을 알게 되었을 것이다. 그러나 smart_pointer는 다른 함수간의 정보전달을 더 쉽게 해 주기도 한다. 예를 들어 C언어에서의 strdup()과 같은 함수는 내부에 malloc을 해서 그 포인터를 리턴하는데 malloc된 메모리를 free하는 것은 프로그래머의 몫이다. 그러나 smart_pointer는 그것을 알아서 해 준다. 다음 예제를 보자.
String strdup(String s){ String ret = new string(*s); return ret;} // 리턴하면서 argument로 쓰였던 ``hello''는 destruct된다.void f(void){ String res = strdup(new string(``hello''));} // 리턴하면서 리턴 값으로 받았던 ``hello''는 destruct된다.
strdup이라는 함수에서 동적인 메모리를 할당 받고 그 smart_pointer를 리턴하고 f라는 함수에서는 그 리턴받은 pointer에 대해서 메모리에 관한 신경을 써 주지 않아도 자동으로 free를 시켜 준다. 물론 strdup에서 argument로 받은 동적인 메모리도 strdup이 리턴하면서 free된다.
이번에는 더욱 복잡한 자료구조에 응용을 해 보자.
#include <iostream>#include <string>#include <vector>#include <map>#include ``smart_pointer.h''int main(void){ typedef smart_pointer<string> String; typedef smart_pointer<vector<String> > Vector; typedef smart_pointer<map<String, Vector> > Map; String kyusic = ``kyusic''; String smiletw = ``taewook''; String mrp = ``jaehueng''; String kanzaki = ``junsung''; Map address_list = new map<String, Vector>; Vector tmpv = new vector<String>; tmpv->push_back(``kyusic@myscan.org''); tmpv->push_back(``kyusic@hotmail.com''); tmpv->push_back(``kyusic@hihome.com''); (*address_list)[kyusic] = tmpv; tmpv = new vector<String>; tmpv->push_back(``smiletw@myscan.org''); tmpv->push_back(``smiletw@illusion.snu.ac.kr''); (*address_list)[smiletw] = tmpv; tmpv = new vector<String>; tmpv->push_back(``ppajae@myscan.org''); (*address_list)[mrp] = tmpv; tmpv = new vector<String>; tmpv->push_back(``willpower@myscan.org''); (*address_list)[kanzaki] = tmpv;}
이렇게 복잡한 자료구조이지만 main이 리턴할 때는 동적으로 할당한 메모리를 모두 free시킨다. 그리고 map이나 vector내부에서는 smart pointer object자체만 복사될 뿐(smart pointer object는 기껏해야 8byte이다.) smart pointer가 가리키고 있는 object는 복사되지 않는다.
물론 c++에서 쓸데 없는 복사를 막기 위해서 pointer를 쓸 수 있겠지만 위처럼 자료구조가 복잡해지기 시작하면 웬만큼 실수를 막지 않고서는 메모리 관리를 하기 힘들어진다. smart_pointer를 사용하는 경우 자바와 같이 복잡한 메모리 구조를 garbage collection보다는 효율적(이라고 나는 생각하고 있다.)인 방식으로 쉽게 다룰 수 있을 것이다.
이 외에 smart_pointer는 ==, <, >에 대한 operator도 정의해 두었는데 그것은 refernce되고 있는 object의 그것들과 같은 효과가 나게 했다. 따라서 다음을 주의해야 한다.
typedef smart_pointer<string> String;String a = ``hi'';String b = ``hi'';if (a == b) { cout << ``two strings are equal.'' << endl;}if (a.is(b)) { cout << ``two strings are the same ones.'' << endl;}
즉 == operator가 smart_pointer가 가리키고 있는 두 object이 equivalent한 것인지를 테스트 하는 것이다.1 두 smart_pointer가 하나의 object를 가리키고 있는지를 테스트하려면 is() method를 사용하면 된다.
그리고 smart_pointer가 가리키는 object에 접근을 하기 위해서 * operator와 ->operator를 정의해 두었다. 앞의 예제에서도 보았듯이 string을 출력하기 위해서 smart_pointer를 직접 출력하지 않고 * operator를 사용한 것을 보라. 그리고 가리키는 object가 class object인 경우가 대다수일 것이므로 ->는 유용하게 쓰일 수 있다.
smart_pointer<string> a = ``hi'';cout << *a << endl;cout << a->size() << endl;
마치 smart_pointer를 보통의 pointer쓰듯이 쓰면 된다.
지금까지 C++의 constructor와 assignment operator를 이용하여 어떻게 메모리 관리를 할수 있는지 알아 보았고, 그것을 smart_pointer라는 클래스로 구현하여 어떻게 쓸 수 있는지도 보았다. 그리고 그 쓰임새가 마치 자바의 reference type variable을 다루는 것고 비슷하다는 것도 살펴 보았다. smart_pointer는 복잡한 자료구조를 다룰 때 위력을 발휘한다. C++프로그래밍 하는데 도움이 되었으면 하는 바이다.
[출처 : http://gc.myscan.org ]
