CodeNote
Coding Note
CodeNote
전체 방문자
오늘
어제
  • 전체 보기 (35)
    • C++ (33)
      • Modern C++ (12)
      • Modern C++ STL (0)
      • Thread (16)
      • Thread (Async) (5)
    • 디자인패턴 (0)
    • Algorithm (2)
    • Electron (0)
    • Python (0)

블로그 메뉴

  • 홈
  • Github
  • 태그
  • 방명록

공지사항

인기 글

태그

  • LOCK
  • mutex
  • C++ #Memory
  • C++
  • Free
  • 자료구조

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
CodeNote

Coding Note

C++/Modern C++

스마트 포인터 문제 - 순환 참조

2022. 10. 3. 22:07

스마트 포인터 (Smart Pointer)


레퍼런스 카운팅 기반의 스마트 포인터 구현

Reference Counting

#pragma once

/*---------------------
*	RefCountable
---------------------*/

class RefCountable
{
public:
	RefCountable() : m_refCount(1) {}
	virtual ~RefCountable() {}

	int32 GetRefCount() const { return m_refCount; }
	int32 AddRef() { return ++m_refCount; }
	int32 ReleaseRef()
	{
		int32 refCount = --m_refCount;
		if (refCount == 0)
		{
			delete this;
		}
		return refCount;
	}
protected:
	atomic<int32> m_refCount;
};

/*------------------------
*	SharedPtr
*	1) 이미 만들어진 클래스 대상으로 사용불가(외부라이브러리 사용하는 경우)
*	2) 순환 (Cycle) 문제 (표준 shared_ptr도 같은 문제)
------------------------*/

template<typename T>
class TSharedPtr
{
public:
	TSharedPtr() {}
	TSharedPtr(T* ptr) { Set(ptr); }
	~TSharedPtr() { Release(); }

	// 복사
	TSharedPtr(const TSharedPtr& rhs) { Set(rhs.m_ptr);	}
	// 이동
	TSharedPtr(TSharedPtr&& rhs) { m_ptr = rhs.m_ptr; rhs.m_ptr = nullptr; }
	// 상속 관계 복사
	template<typename U>
	TSharedPtr(const TSharedPtr<U>& rhs) { Set(static_cast<T*>(rhs.m_ptr)); }

	// 복사 연산자
	TSharedPtr& operator=(const TSharedPtr& rhs)
	{
		if (m_ptr != rhs.m_ptr)
		{
			Release();
			Set(rhs.m_ptr);
		}
		return *this;
	}
	// 이동 연산자
	TSharedPtr& operator=(TSharedPtr&& rhs)
	{
		Release();
		m_ptr = rhs.m_ptr;
		rhs.m_ptr = nullptr;
		return *this;
	}
	bool		operator==(const TSharedPtr& rhs) const { return m_ptr == rhs.m_ptr; }
	bool		operator==(T* ptr) const { return m_ptr == ptr; }
	bool		operator!=(const TSharedPtr& rhs) const { return m_ptr != rhs.m_ptr; }
	bool		operator!=(T* ptr) const { return m_ptr != ptr; }
	bool		operator<(const TSharedPtr& rhs) const { return m_ptr < rhs.m_ptr; }
	T*			operator*() { return m_ptr; }
	const T*	operator*() const { return m_ptr; }
				operator T*() const { return m_ptr; }
	T*			operator->() { return m_ptr; }
	const T*	operator->() const { return m_ptr; }

	bool IsNull() const { return m_ptr == nullptr; }
private:
	inline void Set(T* ptr)
	{
		m_ptr = ptr;
		if (ptr)
			ptr->AddRef();
	}
	inline void Release()
	{
		if (m_ptr)
		{
			m_ptr->ReleaseRef();
			m_ptr = nullptr;
		}
	}
private:
	T* m_ptr = nullptr;
};


위와 같이 직접 구현한 스마트 포인터나 표준 shared_ptr는 모두 순환 참조를 하는 문제가 있다.

간단한 순환 참조 예제

using KnightRef = TSharedPtr<class Knight>;

class Knight : public RefCountable
{
public:
	Knight() { cout << "Knight()" << endl; }
	~Knight() { cout << "~Knight()" << endl; }

	void SetTarget(KnightRef target)
	{
		m_target = target;
	}

	KnightRef m_target = nullptr;
};

int main()
{
	KnightRef k1(new Knight());
	k1->ReleaseRef();
	KnightRef k2(new Knight());
	k2->ReleaseRef();

	k1->SetTarget(k2);
	k2->SetTarget(k1);

	k1 = nullptr;
	k2 = nullptr;
}

k1과 k2가 서로 간에 Ref로 잡고 있기 때문에 레퍼런스 카운트가 절대로 0이 될 수 없어 메모리가 해제되지 못한다. 하지만 이렇게 단순하게 발생하는 순환 참조 경우는 많이 없다.

일반적으로는 컴포넌트 패턴에서 어떤 클래스가 다른 클래스를 포함하는 경우 자주 발생한다.
Knight 클래스에서 inventory 클래스를 가지고 있다고 가정한다.

using KnightRef = TSharedPtr<class Knight>;
using InventoryRef = TSharedPtr<class Inventory>;

class Knight : public RefCountable
{
public:
	Knight() { cout << "Knight()" << endl; }
	~Knight() { cout << "~Knight()" << endl; }

	void SetTarget(KnightRef target)
	{
		m_target = target;
	}

	KnightRef m_target = nullptr;
	InventoryRef m_inventory = nullptr; // inventory를 포함한다.
};

class Inventory : public RefCountable
{
public:
	// Inventory에서 Knight에 접근하는 경우가 있을 수 있다.
	// Knight에서는 Inventory를 가지고 있는데 Inventory도 Knight를 알고있는 상황
	Inventory(KnightRef knight) : m_knight(**knight)
	{

	}

	// 이와 같이 참조로 받으면 레퍼런스 카운트가 증가하지 않으므로 문제 없다.
	Knight& m_knight; 
	// 하지만 아래와 같이 스마트포인터로 받게되면 
	// Knight와 Inventory는 서로 참조하는 상황이 되어 메모리해제가 되지 않는다.
	KnightRef m_knight;
};


표준 shared_ptr 과 weak_ptr

위에서는 반드시 RefCountable을 상속받아야하는 형식으로 처리하고 있지만 표준에서는 그렇지 않다.
내부에 구현 형식이 약간 다르다는 것이다.

표준 shared_ptr과 weak_ptr을 까보면 _Ptr_base를 상속받고 있다.
-> class _Ptr_base { // base class for shared_ptr and weak_ptr

그리고 이 _Ptr_base는 다음과 같이 2가지 포인터를 가지고 있다.

  • element_type* _Ptr{ nullptr } : 객체 타입
  • _Ref_count_base* _Rep{ nullptr } : 레퍼런스 카운팅 블록


RefCountingBlock은 다음의 2가지를 관리한다.

  • Uses : shared_ptr의 레퍼런스 카운트
  • Weaks : weak_ptr의 레퍼런스 카운트

Knight 클래스로 예를 들어보면,

shared_ptr<Knight> spr = make_shared<Knight>();
weak_ptr<Knight> wpr = spr;


내부적으로는 [Knight][RefCoutingBlock(uses, weaks)]와 같은 형태가 될 것이다. 이 때 Knight를 잡고 있는 shared_ptr의 Count가 0이면 Knight의 메모리가 해제되도 weak_ptr의 Count가 0이 아니면 RefCoutingBlock은 계속 살려둔다.

[] [RefCoutingBlock(uses, weaks)] 이런 식으로 될 것이다.
따라서, weak_ptr은 shared_ptr의 메모리가 해제되었는지 확인하는 과정이 필요하다.

1. 해당 shared_Ptr이 아직 유효한지 체크
bool expired = wpr.expired(); 

2. 다시 shared_ptr로 캐스팅해서 Null 체크
shared_ptr<Knight> spr2 = wpr.lock();

그렇다면 weak_ptr을 꼭 사용해야 하는가?

선택의 영역이다. 순환 참조 문제는 예방할 수 있을 것이다. (라이프 사이클에는 영향을 주지않기 때문에)
하지만 막상 사용하면 위의 1, 2번과 같이 번거롭게 확인해야한다.

프로젝트에 따라 어떤 프로젝트에서는 shared_ptr만 자체적으로 만들어서 사용하기도 하고, 표준 shared_ptr과 weak_ptr을 다 사용하기도 한다.

'C++ > Modern C++' 카테고리의 다른 글

[C++] Perfect forwarding  (0) 2022.10.05
[C++] constexpr  (0) 2022.10.05
[C++20] std::span  (0) 2022.10.01
[C++] 문자열 정리  (0) 2022.09.29
[C++17] string_view  (0) 2022.09.07
    'C++/Modern C++' 카테고리의 다른 글
    • [C++] Perfect forwarding
    • [C++] constexpr
    • [C++20] std::span
    • [C++] 문자열 정리
    CodeNote
    CodeNote
    기록 블로그

    티스토리툴바