반응형

 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은

 비어 있는 객체로 초기화 합니다.

make_unique를 통한 객체 생성
복사 연산 수행
이동 연산 수행

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 결과값 (소멸자 호출하지 않음)

 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 함수는 이전에 저장된 원래의 포인터를 할당 해제하고 입력받은 인자 값의 소유권을 가집니다. 

 만약에서 입력된 포인터가 저장된 포인터와 같으면 저장된 포인터를 삭제합니다.

// [예제코드]
// 저장된 포인터와 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

 

Boost.SmartPtr: The Smart Pointer Library - 1.72.0

Smart pointers are objects which store pointers to dynamically allocated (heap) objects. They behave much like built-in C++ pointers except that they automatically delete the object pointed to at the appropriate time. Smart pointers are particularly useful

www.boost.org

https://docs.microsoft.com/ko-kr/cpp/standard-library/unique-ptr-class?view=vs-2019

 

unique_ptr Class

unique_ptr Class In this article --> Stores a pointer to an owned object or array. The object/array is owned by no other unique_ptr. The object/array is destroyed when the unique_ptr is destroyed. Syntax class unique_ptr { public: unique_ptr(); unique_ptr(

docs.microsoft.com

 

반응형

'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

+ Recent posts