스마트 포인터 (Smart Pointer)
RAII (Resource Acquisition Is Initialization)
C++의 창시자인 스트롭스트룹이 제안한 디자인 패턴이다.
C++ RAII(Resource Acquisition Is Initialization)의 해석을 요약하면 다음과 같다.
객체의 유효성에 대해 다음의 관점이 필요하다.
생성이 완료된 객체는 유효한 객체이어야 한다. 즉, 유효하지 않은 객체가 생성되어서는 안 된다.
필요한 자원들을 획득하지 못한 객체는 유효한 객체가 아니다.
대칭성에 의해, 소멸되어서 더 이상 유효하지 않게 된 객체는 어떤 자원도 가지고 있지 않아야 한다.
메모리 누수를 방지하는 기법이다.
동적으로 할당한 메모리, 혹은 파일을
open
한 뒤early return
혹은throw
등 예기치 못하게 스택을 되감는 경우에 메모리 누수가 발생할 수 있다.main.cpp
#include <iostream> #include "Resource.h" using namespace std; void doSomething() { Resource* res = new Resource; if (true) return ; delete res; } int main() { doSomething(); } /* stdout stderr Resource constructed */
Resource.h
#pragma once #include <iostream> class Resource { public: int data_[100]; Resource() { std::cout << "Resource constructed\n"; } ~Resource() { std::cout << "Resource destoryed\n"; } };
auto_ptr
C++98부터
auto_ptr
클래스가 존재했는데, 앞으로는 사용하지 않아야 되는 클래스이다.- 예기치 못한 상황이 발생할 수 있기 때문에, 더 안정적인
unique_ptr
등을 사용하면 된다.
- 예기치 못한 상황이 발생할 수 있기 때문에, 더 안정적인
스택에 할당된 인스턴스가 자동으로 소멸되는 것을 이용했다.
아래는
auto_ptr
를 비슷하게 구현해보는 예제이다.main.cpp
#include <iostream> #include "Resource.h" #include "AutoPtr.h" using namespace std; void doSomething() { AutoPtr<Resource> res(new Resource); } int main() { doSomething(); } /* stdout stderr Resource constructed Resource destoryed */
Resource.h
#pragma once #include <iostream> class Resource { public: int data_[100]; Resource() { std::cout << "Resource constructed\n"; } ~Resource() { std::cout << "Resource destoryed\n"; } };
AutoPtr.h
#pragma once #include <iostream> template<class T> class AutoPtr { public: T* ptr_ = nullptr; AutoPtr(T *ptr = nullptr) : ptr_(ptr) {} ~AutoPtr() { if (ptr_ != nullptr) delete ptr_; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } bool isNull() const { return ptr_ == nullptr; } };
Move Semantics
다음과 같은 경우 double free 문제가 발생하여 런타임 에러를 만날 수 있다.
main.cpp
#include <iostream> #include "Resource.h" #include "AutoPtr.h" using namespace std; void doSomething() { AutoPtr<Resource> res1(new Resource); AutoPtr<Resource> res2; cout << boolalpha; cout << res1.ptr_ << endl; cout << res2.ptr_ << endl; res2 = res1; cout << res1.ptr_ << endl; cout << res2.ptr_ << endl; } // 여기서 소멸자가 호출될 때 메모리를 두 번 해제하게 되어 에러 발생 int main() { doSomething(); }
Resource.h
#pragma once #include <iostream> class Resource { public: int data_[100]; Resource() { std::cout << "Resource constructed\n"; } ~Resource() { std::cout << "Resource destoryed\n"; } };
AutoPtr.h
#pragma once #include <iostream> template<class T> class AutoPtr { public: T* ptr_ = nullptr; AutoPtr(T *ptr = nullptr) : ptr_(ptr) {} ~AutoPtr() { if (ptr_ != nullptr) delete ptr_; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } bool isNull() const { return ptr_ == nullptr; } };
이런 상황을 방지하려면
copy construnctor
와assignment operator
를 오버로딩해야 한다.AutoPtr
클래스가 복사 생성자를 호출하거나 대입 연산을 할 때, 내부의 포인터가 옮겨가는 방식으로 구현한다.이렇게 되면 같은 포인터를 한 인스턴스만 갖도록 하여 메모리 해제의 중복 문제를 피할 수 있다.
main.cpp
#include <iostream> #include "Resource.h" #include "AutoPtr.h" using namespace std; void doSomething() { AutoPtr<Resource> res1(new Resource); AutoPtr<Resource> res2; cout << boolalpha; cout << res1.ptr_ << endl; cout << res2.ptr_ << endl; res2 = res1; // move semantics cout << res1.ptr_ << endl; cout << res2.ptr_ << endl; } int main() { doSomething(); } /* stdout stderr Resource constructed 00E8E518 00000000 00000000 00E8E518 Resource destoryed */
Resource.h
#pragma once #include <iostream> class Resource { public: int data_[100]; Resource() { std::cout << "Resource constructed\n"; } ~Resource() { std::cout << "Resource destoryed\n"; } };
AutoPtr.h
#pragma once #include <iostream> template<class T> class AutoPtr { public: T* ptr_ = nullptr; AutoPtr(T *ptr = nullptr) : ptr_(ptr) {} AutoPtr(T& a) { ptr_ = a.ptr_; a.ptr_ = nullptr; } ~AutoPtr() { if (ptr_ != nullptr) delete ptr_; } AutoPtr& operator = (AutoPtr& a) { if (&a == this) return *this; delete ptr_; ptr_ = a.ptr_; a.ptr_ = nullptr; return *this; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } bool isNull() const { return ptr_ == nullptr; } };
Syntax vs Semantics
AutoPtr의 한계
다음과 같이 함수 안으로
move semantics
가 적용되는 경우, 해당 함수가 끝나면서 메모리가 해제된다.- 이후 해당 인스턴스가 또 소멸되는 상황이므로 런타임 에러가 발생한다.
#include <iostream> #include "Resource.h" #include "AutoPtr.h" using namespace std; void doSomething2(AutoPtr<Resource> res) { } void doSomething() { AutoPtr<Resource> res1(new Resource); doSomething2(res1); } // 에러 int main() { doSomething(); }
'C++ > SmartPointer' 카테고리의 다른 글
C++ 순환 의존성 문제 (Circular Dependency Issues) (0) | 2021.03.24 |
---|---|
C++ 이동 생성자와 이동 대입 (Move Constructor and Move Assignment) (0) | 2021.03.24 |
C++ R-value Reference (0) | 2021.03.24 |
C++ Syntax vs Semantics (0) | 2021.03.24 |