태크놀로지

[C++] unique_ptr 만들기 - 스마트 포인터가 제공하는 메서드와 연산자 구현 (+ reset / release 차이점 이해) 본문

C++

[C++] unique_ptr 만들기 - 스마트 포인터가 제공하는 메서드와 연산자 구현 (+ reset / release 차이점 이해)

원택 2020. 8. 7. 14:07

스마트 포인터 연산자 & 메서드 구현하기

이전에 만들었던 유니크 포인트에서 unique_ptr에서 제공하는 메서드와 연산자를 분석해보며 구현해보도록 합시다.

 

<구현할 메서드 목록>

get / release / reset / operator-> /operator=

 


STL std::unique_ptr 코드 분석

- get / operator->

_NODISCARD pointer operator->() const noexcept {
     return _Mypair._Myval2;
 }

 _NODISCARD pointer get() const noexcept {
     return _Mypair._Myval2;
 }

유니크 포인터 객체의 포인터를 반환해줍니다.

 

- release

pointer release() noexcept {
    return _STD exchange(_Mypair._Myval2, pointer());
}
// FUNCTION TEMPLATE exchange
template <class _Ty, class _Other = _Ty>
_Ty exchange(_Ty& _Val, _Other&& _New_val) noexcept(
    conjunction_v<is_nothrow_move_constructible<_Ty>, is_nothrow_assignable<_Ty&, _Other>>) /* strengthened */ {
    // assign _New_val to _Val, return previous _Val
    _Ty _Old_val = static_cast<_Ty&&>(_Val);
    _Val         = static_cast<_Other&&>(_New_val);
    return _Old_val;
}

release 호출시 현재 객체 포인터를 반환하고, 디폴트 생성자를 저장하여 객체를 초기화해줍니다. release 함수를 호출시 주의해야할점은 이전에 저장된 메모리가 해제되지 않기 때문에 함수에서 객체를 반환시 반드시 메모리 해제를 해주어야합니다.

 

코드 테스트

std::unique_ptr<TEST> tt = std::make_unique<TEST>(4);
TEST* tptr = tt.release();
cout << tptr->a << endl;

if (tt)
	cout << tt->a << endl;
else
	std::cout << "nullptr입니다." << std::endl;

결과창

위와 같이 unique_ptr의 release함수를 호출후에 스마트포인터의 객체 포인터는 nullptr로 입력되지만, 이전에 저장된 객체 자체는 메모리가 남아있는것을 볼수 있습니다.

 

- reset

void reset(pointer _Ptr = pointer()) noexcept {
    pointer _Old = _STD exchange(_Mypair._Myval2, _Ptr);
    if (_Old) {
        _Mypair._Get_first()(_Old);
    }
}

reset 메서드 또한 release와 비슷한 동작을 하고있습니다. 대신 함수인자로 객체 포인터를 전달하였을시, 반환하는것이 아닌 교체를 해주고 있습니다. 그리고 이전 메모리가 있을시 Mypair operator()로 디폴트 소멸생성자를 호출하여 메모리를 해제해줍니다.

- operator=

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;
}

이동 대입연산자로 rValue값을 저장하고 해제시켜주고 있습니다.

 


커스텀 스마트 포인터 메서드 구현

- release

/* 주의! 객체를 반환시 꼭 메모리 해제를 해주어야한다.
@ 현재 객체 포인터를 반환하고,
@ 디폴트 생성자를 저장하여 객체를 초기화한다.
@ return 초기화 전 객체 포인터
*/
T* release()
{
	// val: cur value / newVal: default
	T* oldValue = pointer;
	pointer = nullptr;
	return oldValue;
}

객체의 소유권만 변경시켜줘야하기 때문에 포인터를 변경 후, 이전 객체에 대한 소유권을 반환하였습니다.

- reset

/*
@ 객체를 초기화 해준다.
*/
void reset(T* newPointer = nullptr)
{
	if(pointer) delete pointer;
	pointer = newPointer;
}

현재 객체가 존재한다면, 객체를 해제시켜주고 함수인자로 전달된 파라미터값을 저장해줍니다.

 

- get / operator->

// pointer 접근 메서드 및 연산자
T* get() const { return pointer; };
T* operator-> () const { return pointer; }

객체의 포인터값을 전달해줍시다.

 

- 수정된 이동생성자

// 이동생성자
unique_ptr(unique_ptr&& rhs)
{
	if (this != std::addressof(rhs))
	{
		T* rhsPtr = rhs.release();
		reset(rhsPtr);
	}
}
unique_ptr& operator= (unique_ptr&& rhs)
{
	// if(this != r value) reset -> swap
	if (this != std::addressof(rhs))
	{
		T* rhsPtr = rhs.release();
		reset(rhsPtr);
	}
	return *this;
}

앞서 제작한 메서드를 사용하여 이동생성자와 이동연산자를 간단하게 코드정리합시다.

※ new와 delete는 생성 관리하는 함수를 통해 접근하는게, 코드 안정성을 더 높여줍니다.

 


커스텀 유니크포인트 전체 코드

#pragma once

namespace wt
{
	template <class T>
	class unique_ptr
	{
	private:
		T* pointer;

	public:
		// 기본생성자
		unique_ptr() {}
		unique_ptr(T* ptr) : pointer(ptr) {}
		~unique_ptr()
		{
			if (pointer != nullptr) delete pointer;
			pointer = nullptr;
		}
		// 복사생성자 삭제 (이동만 가능하도록)
		explicit unique_ptr(const unique_ptr& rhs) = delete;
		unique_ptr& operator= (const unique_ptr& rhs) = delete;

		// 이동생성자
		unique_ptr(unique_ptr&& rhs)
		{
			if (this != std::addressof(rhs))
			{
				T* rhsPtr = rhs.release();
				reset(rhsPtr);
			}
		}
		unique_ptr& operator= (unique_ptr&& rhs)
		{
			// if(this != r value) reset -> swap
			if (this != std::addressof(rhs))
			{
				T* rhsPtr = rhs.release();
				reset(rhsPtr);
			}
			return *this;
		}

		/*
		@ 객체를 초기화 해준다.
		*/
		void reset(T* newPointer = nullptr)
		{
			if(pointer) delete pointer;
			pointer = newPointer;
		}

		/* 주의! 객체를 반환시 꼭 메모리 해제를 해주어야한다.
		@ 현재 객체 포인터를 반환하고,
		@ 디폴트 생성자를 저장하여 객체를 초기화한다.
		@ return 초기화 전 객체 포인터
		*/
		T* release()
		{
			// val: cur value / newVal: default
			T* oldValue = pointer;
			pointer = nullptr;
			return oldValue;
		}

		// pointer 접근 메서드 및 연산자
		T* get() const { return pointer; };
		T* operator-> () const { return pointer; }
	};

	template <class T, class... Args>
	unique_ptr<T> make_unique(Args... args)
	{
		return unique_ptr<T>(new T(args...));
	}
}