c++에서는 포인터를 사용자에게 제공함으로써 유연성과 성능을 최대화 할 수 있도록 지원했습니다.
이러한 이점도 있었지만 한편으로는 포인터를 통한 잘못된 메모리 할당과 관리로 인한 문제가 많이 발생되었습니다.
// 힙에서 할당한 int 주소를 스택에 저장 후 delete없이 함수를 벗어난다.
int * pNewInt = new int;
if (pNewInt)
{
*pNewInt = 0x1234;
}
return;
int * pNewInt = new int;
if (pNewInt)
{
*pNewInt = 0x1234;
delete pNewInt;
}
// 많은 구문을 실행합니다.
// ...
// 이미 해제된 메모리를 중복으로 해제 합니다.
delete pNewInt;
return 0;
그로인해 포인터의 이점을 가져가면서 더욱 안전하게 관리 될 수 있는 방법을 필요했는데 그 해결법으로
c++ 11부터 boost에서 제공되던 스마트 포인터가 stl에 추가되었습니다.
스마트 포인터는 템플릿으로 구현되어 있기 때문에 소스의 구현내용을 확인 할 수 있습니다.
memory로 되어 있는 헤더 파일로 접근하면 스마트 포인터에 대한 모든 구현을 확인 할 수 있습니다.
( visual studio에서는 객체의 데이터형을 클릭한 후 F12를 누르면 해당 구현 위치로 이동합니다.)
다양한 스마트 포인터 중에 이번 장에서는 unique_ptr에 대해서 설명 드립니다.
unique_ptr 특징
unique_ptr은 자원(포인터)을 고유하게 관리합니다. 즉 하나의 자원을 하나의 unique_ptr의 객체만 소유 할 수
있습니다.
자원을 소유한 오브젝트가 소멸되면 자동적으로 자원(메모리)을 해제합니다.( Scope 벗어나면 해제 )
unique_ptr은 copy 할 수 없지만 move는 지원합니다. (move가 되어도 객체는 하나의 unique_ptr만 소유한다.)
또한 사용을 편하기 위해서 ->연산자를 오버로딩해서 사용하면 저장된 포인터로 접근합니다.
각 특징들을 어떻게 구현하는지 소스 코드를 통해서 확인 해봅니다.
// [Memory에 선언된 unique_ptr 소스코드]
template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr
{
public:
//... 수행구문
// unique_ptr은 복사생성자와 대입연산자으로 delete로 선언합니다.
// unique_ptr 객체끼리의 대입연산자와 복사생성자는 실패합니다.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
private:
// unique_ptr은 _Mypair라는 객체를 가지는데 std::pair와 비슷합니다.
// first에는 _Dx. 즉 deleter 함수 객체를 저장합니다.
// second에는 저장하고 있는 포인터 객체를 저장합니다.
_Compressed_pair<_Dx, pointer> _Mypair;
}
// [예제 코드]
auto pSimple = std::make_unique<Sample>(10);
// 대입 연산자 실패 error 1776
auto pSimple2 = pSimple;
// 복사 생성자 실패 error 1776
std::unique_ptr<Sample> pSimple3(pSimple);
위의 unique_ptr의 소스 코드를 보면 복사 생성자와 대입 연산자를 delete로 선언해서 객체의 복사를 방지 합니다.
_Mypair 객체를 소유하고 있는데 앞으로 소스코드를 이해하기 위해서 저장된 정보를 알고 있어야 합니다.
std::pair와 비슷한 구조로 first에는 객체를 삭제 할 deleter ( default_delete )가 저장되며 second에는 저장하고 있는
포인터 객체를 저장합니다.
// [Memory에 선언된 unique_ptr 소스코드]
// unique_ptr의 소멸자 함수
~unique_ptr() noexcept {
// 포인터 객체가 살아 있다면
// 등록된 deleter 함수 객체로 포인터 객체를 해제합니다.
if (_Mypair._Myval2) {
_Mypair._Get_first()(_Mypair._Myval2);
}
}
unique_ptr의 소멸자 함수입니다. 해당 함수에서 객체 소유한 포인터가 있다면 등록된 삭제 함수 객체로
소유한 포인터를 해제를 진행합니다. 그렇기 때문에 사용자는 메모리 할당 해제에 대해서 신경쓰지 않고 편하게
사용할 수 있습니다.
// [Memory에 선언된 unique_ptr 소스코드]
// [unique_ptr의 이동 생성자]
unique_ptr(unique_ptr&& _Right) noexcept
: _Mypair(_One_then_variadic_args_t(), _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {}
// [unique_ptr의 이동 대입 연산자]
unique_ptr& operator=(unique_ptr&& _Right) noexcept {
if (this != _STD addressof(_Right)) {
reset(_Right.release());
_Mypair._Get_first() = _STD forward<_Dx>(_Right._Mypair._Get_first());
}
return *this;
}
unique_ptr이 이동연산자를 지원한다고 햇는데 unique_ptr은 위처럼 구현해서 이동연산자를 구현합니다.
unique_ptr의 release() 함수는 자신이 가진 포인터 객체 정보를 초기화하고 리턴값으로 이전에 들고 있던 포인터를
리턴합니다.
// [예제 코드]
std::unique_ptr<int> pInt = std::make_unique<int>(10);
// 이동 대입 연산자 수행!
std::unique_ptr<int> pMoveTargetInt = std::move(pInt);
이동 연산자를 수행하면 unique_ptr이 소유한 자원을 Target unique_ptr에게 이동시키고 unique_ptr은
비어 있는 객체로 초기화 합니다.
unique_ptr의 생성 방법
// [Memory에 선언된 unique_ptr 소스코드]
// unique_ptr은 템플릿으로 구현되어 있습니다.
// 템플릿 인자로 _Ty와 _Dx를 전달 받습니다.
// _Ty : 저장할 포인터의 데이터 형
// _Dx : 삭제시 실행할 함수 객체 (기본형은 default_delete<_Ty>)
template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr
{
// 내부 수행 함수
}
// [Memory에 선언된 소스코드]
// 기본적인 delete를 수행하는 함수 객체
template <class _Ty>
struct default_delete { // default deleter for unique_ptr
constexpr default_delete() noexcept = default;
template <class _Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0>
default_delete(const default_delete<_Ty2>&) noexcept {}
void operator()(_Ty* _Ptr) const noexcept /* strengthened */ { // delete a pointer
static_assert(0 < sizeof(_Ty), "can't delete an incomplete type");
delete _Ptr;
}
};
템플릿 인자 | 설 명 |
_Ty | unique_ptr로 관리할 포인터의 데이터형을 전달합니다 |
_Dx | 포인터 객체를 할당 해제 할 때 사용할 함수 객체입니다.(입력하지 않으면 default_delete 설정) |
// int의 포인터를 가지는 unique_ptr
std::unique_ptr<int> pUInt(new int(10));
int형의 unique_ptr를 호출하고 싶다면 위와 같이 선언 할 수 있습니다.
make_unique를 사용한 unique_ptr 생성
unique_ptr를 생성 할 때 make_unique를 사용할 수 있습니다. make_unique는 예외에 대해서 안전하게 구현되어
있어서 unique_ptr의 생성자을 직접 호출하는 것보다 make_unique를 사용하는 것이 좋습니다.
// [예제 코드]
// int의 포인터를 가지는 unique_ptr을 make_unique를 사용해서 생성
// 예외에 안전하게 구현되어 있습니다.! make_unique 권장
auto pSimple = std::make_unique<int>(10);
자주 사용하는 함수 사용 방법
struct Sample {
int content_;
Sample(int content) : content_(content) {
std::cout << "Constructing Sample(" << content_ << ")" << std::endl;
}
~Sample() {
std::cout << "Deleting Sample(" << content_ << ")" << std::endl;
}
void print(){
std::cout << "Sample Print (" << content_ << ")" << std::endl;
}
};
예제를 설명하기 위해서 Sample 구조체를 사용합니다.
// [Memory에 선언된 unique_ptr 소스코드]
// [* 연산자 오버로딩]
// 설명 : 저장된 포인터를 포인터 형으로 리턴합니다.
_NODISCARD add_lvalue_reference_t<_Ty> operator*() const noexcept /* strengthened */ {
return *_Mypair._Myval2;
}
// [-> 연산자 오버로딩]
// 설명 : 저장된 포인터를 포인터 형으로 리턴합니다.
_NODISCARD pointer operator->() const noexcept {
return _Mypair._Myval2;
}
// [예제 코드]
auto pSample = std::make_unique<Sample>(10);
// -> 연산자 사용해서 Sample 포인터 직접 접근
pSample->print();
// * 연산자 사용해서 Sample 레퍼런스
Sample & s = *pSample;
unique_ptr의 ->연산자와 * 연산자를 오버로딩해서 저장된 포인터에 직접 접근하는 방법을 제공해서
포인터처럼 동작하도록 지원합니다.
// [get 함수]
// 설명 : 저장된 포인터를 포인터 형으로 리턴합니다.
pointer get() const;
// [예제 코드]
auto pSample = std::make_unique<Sample>(10);
// pOriginSample는 pSample의 저장된 포인터 주소를 가집니다.
Sample * pOriginSample = pSample.get();
get 함수는 unique_ptr에 저장된 포인터를 포인터 형태로 리턴합니다.
// [release 함수]
// 설명 : unique_ptr의 저장된 포인터를 nullptr로 셋팅하고 이전에 저장하던 포인터를 리턴합니다.
pointer release();
// [예제 코드]
auto pSample = std::make_unique<Sample>(10);
// pSample는 nullptr을 가집니다.
Sample * pOriginSample = pSample.release()
// delete pOriginSample;
// 를 수행해야합니다.
release 함수는 unique_ptr에 저장된 포인터를 리턴값으로 리턴하고, 저장된 포인터를 nullptr로 셋팅합니다.
주의를 해야할 점은 메모리를 해제하지 않기 때문에 리턴값을 전달받는 주체가 메모리 할당 해제를 수행 해야 합니다.
// [reset 함수]
// 설명 : 저장된 포인터를 할당 해제하고 전달 받은 인자의 소유권을 획득합니다.
void reset(pointer ptr = pointer());
void reset(nullptr_t ptr);
// [예제 코드]
auto pSample = std::make_unique<Sample>(10);
pSample.reset(new Sample(20));
reset 함수는 이전에 저장된 원래의 포인터를 할당 해제하고 입력받은 인자 값의 소유권을 가집니다.
만약에서 입력된 포인터가 저장된 포인터와 같으면 저장된 포인터를 삭제합니다.
// [예제코드]
// 저장된 포인터와 reset 입력인자 포인터가 같으면 비정상 동작합니다.
auto pSample = std::make_unique<Sample>(10);
// 저장된 포인터와 같은 포인터를 reset에 입력하면 힙이 깨진다!! VS2019임
pSample.reset(pSample.get());
( VS 2019에서는 힙이 오염되서 스코프 벗어날때 프로세스가 종료됩니다.)
특수한 deleter 선언
만약에 특수한 커스텀 삭제용 함수 객체를 사용하고 싶다면 아래와 같은 예제로 생성 할 수 있습니다.
// default_delete 대신에 내가 작성한 delete를 사용합니다.
// 삭제된 객체의 갯수에 대해서 카운팅합니다.
template <class _Ty>
struct My_delete { // default deleter for unique_ptr
constexpr My_delete() noexcept = default;
template <class _Ty2, std::enable_if_t<std::is_convertible_v<_Ty2*, _Ty*>, int> = 0>
My_delete(const My_delete<_Ty2>&) noexcept {}
void operator()(_Ty* _Ptr) const noexcept /* strengthened */ { // delete a pointer
static_assert(0 < sizeof(_Ty), "can't delete an incomplete type");
s_deleteCount.fetch_add(1);
delete _Ptr;
}
private:
static std::atomic<DWORD> s_deleteCount;
};
template <class _Ty>
std::atomic<DWORD> My_delete<_Ty>::s_deleteCount;
// 실제 호출시
std::unique_ptr<int, My_delete<int>> pUInt;
이제까지 unique_ptr에 대해서 알아 보았습니다. 참조가 필요하지 않는 포인터 객체가 있다면 기존의 포인터 대신에
안전한 unique_ptr를 사용하기를 권장드립니다.!
unique_ptr 관련 사이트 주소
스마트 포인터에 대해서 더 자세하게 알고 싶으신 분들을 위해서 boost와 msdn의 unique_ptr의 문서를 링크드립니다.
https://www.boost.org/doc/libs/1_72_0/libs/smart_ptr/doc/html/smart_ptr.html
https://docs.microsoft.com/ko-kr/cpp/standard-library/unique-ptr-class?view=vs-2019
'c++ > modern c++' 카테고리의 다른 글
[c++] std::move와 std::forward (4) | 2020.03.07 |
---|---|
[c++] Move semantics (0) | 2020.03.07 |
[c++] 람다(Lambda) 표현식 (0) | 2020.03.04 |
[c++] weak_ptr (1) | 2020.03.02 |
[c++] shared_ptr (0) | 2020.02.29 |