C+11 부터, BOOST 라이브러리 들의 boost::shared_ptr, boost::scoped_ptr 등은 표준화 되어, std::unique_ptr, std::shared_ptr 들이 되었습니다. 개인적으로 스마트 포인터를 직접 만들어 보는 것은 별로 좋은 생각이 아닙니다. 스마트 포인터들의 작동방식은 간단해보이지만, 실제로 스마트 포인터들은 모든 맥락에서 올바르게 작동하도록 노력한 라이브러리 이기 때문입니다. 요번 글에서는 Smart Pointer 중 하나인 Unique Ptr 의 사용법 과 간단한 예시를 보입니다.
먼저 Unique_ptr 이란 ? (cpp reference 발췌)
std::unique_ptr is a smart pointer that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope.
std::unique_ptr은 스마트 포인터로서, 다른 object에 대해, 메모리 관리와 실행에 관한 권한을 얻고, scope 가 벗어나게 되었을때에는 메모리를 해제해주는 포인터입니다.
The object is disposed of, using the associated deleter when either of the following happens:
- the managint unique_ptr object is destroyed
- the managine unique_ptr object is assigned another pointer vis operator= or reset( )
객체는 다음과 같은 상황이 일어나게 되었을때, 메모리를 해제하게 됩니다.
- unique_ptr 객체가 없어졌을때,(scope 밖을 벗어났을떄.)
- unique_ptr 객체가 다른 포인터에게 넘겨 졌을때, (unique_ptr 객체를 할당하거나 넘겨줄때.!)
Unique_ptr 의 기본적인 사용법
#include <memory>
#include <iostream>
#include <stdexcept>
#include <cassert>
// helper class for runtime polymorphism demo below
struct B
{
virtual ~B() = default;
virtual void bar() { std::cout << "B::bar\n"; }
};
struct D : B
{
D() { std::cout << "D::D\n"; }
~D() { std::cout << "D::~D\n"; }
void bar() override { std::cout << "D::bar\n"; }
};
int main()
{
std::cout << "1) Unique ownership semantics demo\n";
{
// Create a (uniquely owned) resource
std::unique_ptr<D> p = std::make_unique<D>();
// Transfer ownership to `pass_through`,
// which in turn transfers ownership back through the return value
std::unique_ptr<D> q = pass_through(std::move(p));
// `p` is now in a moved-from 'empty' state, equal to `nullptr`
assert(!p);
}
std::cout << "\n" "2) Runtime polymorphism demo\n";
{
// Create a derived resource and point to it via base type
std::unique_ptr<B> p = std::make_unique<D>();
// Dynamic dispatch works as expected
p->bar();
}
std::cout << "\n" "3) Custom deleter demo\n";
}
FIRST BASH
- 우선 스마트 포인터 중, Unique_ptr 이 좀 더 완성되기 쉬우므로, unique_ptr 부터 완성해보도록 합시다.
template <typename T>
class U_ptr {
public:
U_ptr(T *d) : data(d) { std::cout << "resource acquired. " << std::endl; }
~U_ptr() {
delete data;
std::cout << "resource disposed. " << std::endl;
}
T *operator->() { return data; }
T &operator*() { return *data; }
T *release() {
T *result = nullptr;
std::swap(result, data);
return result;
}
// for conditional expression
operator bool() { return data; }
private:
T *data;
};
문제점 1 .
“Rule of Three” 즉, 여기서는 복사할당자(copy Constructor) 와 할당연산자(copy assignment operator) 가 정의 되어있지 않아, 어떠한 일이 발생할지 모른다.
만약 여기서, 복사 할당자와 할당연산자를 실행해보면, Segmentation Fault 가 뜨는 것을 볼 수 있다.
{
U_ptr<std::string> d = new std::string("new sample");
U_ptr<std::string> e(d); // copy constructor
// e 를 d 에 복사 할당자로 선언해주는데,
// 같은 포인터를 가르키고 있고, scope 를 벗어나게 되면, d 가 먼저 해제.
// 그리고 e를 해제시켜주려 하는데, 이때
// 이미 해제된 포인터를 또 해제하려고 하기에
// segmentation fault 발생.
d = e; // Assignment Operation
// 마찬가지로 에러 발생.
}
문제점 2 .
- 다음과 같이 암묵적 변환이 이뤄지게 되면서, e_tmp 는 함수에 넣어지게 된후, Scope 를 빠져나오게 되는데, 이때 Unique_ptr 정책에 따라 Scope를 빠져나오면서 객체를 사라지게 하고, 해당 객체에 더 이상 접근이 불가능하게 됩니다.
- 이를 위해, 복사할당자 등등을 수정된 코드에서 접근 불가능하게 할 것입니다.
//중략
void tmp(U_ptr<std::string> tmp) { std::cout << "Is Implicit Happened? " << std::endl; }
int main() {
//U_ptr<std::string> d = new std::string("new sample");
std::string* e_tmp = new std::string("Implicit construction");
tmp(e_tmp);
std::cout << *e_tmp << std::endl; // error occured.
}
/*
마치 다음과 같이 암묵적 변환이 일어난다.
{
Unique_ptr<std::string> tmp(e_tmp);
}
스코프 벗어난 후 소멸..
*/
문제점 3 .
우리가 스마트포인터를 사용하여 접근하게 되었을때, smart pointer 의 상태에 영향을 미치지 않으므로, const 를 사용하여,
다음과 같이 변형이 가능합니다.
// 기존 코드
T* operator->() { return data; }
T& operator*() { return *data; }
// 수정된 코드
T* operator_>() const { return data; }
T& operator*() const { return *data; }
문제점 4 .
다음과 같이 Boolean Conversion 이 너무 쉽게 이루어져, 문제 발생여지가 다분합니다.
예를 들어 밑 코드에서와 같이 컴파일러는, Boolean 으로의 변환을 암묵적으로 하고, 같다고 사용할 겁니다..
U_ptr<std::string> lhs = new std::string("Left Hand Side");
U_ptr<std::string> rhs = new std::string("Right Hand Side");
if (lhs == rhs)
std::cout << "true" << std::endl; // true return
따라서 explicit 을 사용하여, 명시적으로 사용 할 수 있게끔, 해줍니다.
// 기존 코드
operator bool() { return data; }
// 수정된 코드
explicit operator bool() { return data; }
최종 수정 코드
template <typename T>
class U_ptr {
public:
explicit U_ptr() : data(0) {}
explicit U_ptr(T *d) : data(d) {
std::cout << "resource acquired. " << std::endl;
}
~U_ptr() {
delete data;
std::cout << "resource disposed. " << std::endl;
}
// move constructor
U_ptr(U_ptr &&t) {
std::cout << "copied" << std::endl;
if (this != &t) {
data = t.data;
t.data = nullptr;
}
}
// Remove Compiler generated methods
// 스마트 포인터의 객체에 대한 권한 소유권을 유지.
U_ptr(U_ptr const &) = delete;
U_ptr &operator=(const U_ptr &) = delete;
// Const correct access owned object
T *operator->() const { return data; }
T &operator*() const { return *data; }
T *release() {
T *result = nullptr;
std::swap(result, data);
return result;
}
// for conditional expression
explicit operator bool() { return data; }
T *get() const { return data; }
private:
T *data;
};
- 부연 설명 :
- 실제 unique_ptr 포인터 (c++ 표준) 에는 get( ) 함수가 있는데, 이는 unique 라는 의미는 버린채, 포인터를 반납해줌으로써, 객체의 접근 권한을 넘겨줍니다.
- make_unique( ) 함수를 통해서, 템플릿 인자로 된 클래스의 생성자를 완벽하게 전달해줄 수 있습니다.
- 예시를 들어 unique_ptr를 만들어 봤지만, 당연히 실제로 사용하기엔 너무나 부족함이 많습니다.. 표준 unique_ptr 함수를 사용해주세요..!