12
23

C++ 공부하다 보면 RAII라는 말을 꼭 한 번은 듣게 된다.
(Resource Acquisition Is Initialization)

자원은 객체의 생명주기에 묶어서 관리하자

  • 객체가 생성될 때 자원을 얻고
  • 객체가 파괴될 때 자원을 해제한다

파일, 메모리, 뮤텍스 같은 것들이 전부 대상이다.


delete를 호출하면..

 
Foo* foo = new Foo(); // ... delete foo;

이 코드 자체는 틀리지 않는다.
문제는 사람이 틀린다는 점이다.

  • 예외 터지면?
  • return 중간에 하면?
  • 누가 delete 해야 하는지 헷갈리면?

결국 이런 코드가 된다.

delete 했는지 안 했는지 알 수 없는 포인터
RAII는 이걸 구조적으로 막을 수 있음
이걸 가장 단순하게 보여주는 게 unique_ptr이다.

UniquePtr – 소유자는 하나

unique_ptr의 규칙은 명확하다. 소유자는 한명이다

  • 복사 ❌
  • 이동 ⭕
  • 소멸 시 자동 delete

그래서 인터페이스도 이렇게 된다.

 
	// 복사 생성자 대입연산자 삭제
	DD_UniquePtr(const DD_UniquePtr& rhs) = delete;
	DD_UniquePtr& operator= (const DD_UniquePtr& rhs) = delete;

	DD_UniquePtr(DD_UniquePtr&& rhs) noexcept
	{
		this->m_pointer = rhs.m_pointer;
		rhs.m_pointer = nullptr;
		std::cout << "Move Constructor Called" << std::endl;
	}
	DD_UniquePtr& operator= (DD_UniquePtr&& rhs) noexcept
	{
		if (this != &rhs)
		{
			delete m_pointer;
			m_pointer = rhs.m_pointer;
			rhs.m_pointer = nullptr;
		}

		std::cout << "Move Assignment Operator Called" << std::endl;
		return *this;
	}

이 한 줄만 봐도 의미가 전달된다.

복사 대입 X 무조건 std::move를 통해 소유권 전달만 가능

이동 생성자에서는 그냥 소유권만 넘긴다.
스코프를 넘어가거나 들고있는 객체가 사라지게 되면 알아서 소멸자에서 delete를 해준다.
왠만한건 다 유니크로 하면 될듯

더보기
#pragma once

namespace DD
{
	template <class T>
	class DD_UniquePtr
	{
	public:
		T* m_pointer;
	public:
		void reset(T* _Ptr = nullptr) noexcept
		{
			if (_Ptr != m_pointer)
			{
				delete m_pointer;
			}

			m_pointer = _Ptr;
		}
	public:
		DD_UniquePtr() {}
		DD_UniquePtr(T* ptr) : m_pointer(ptr) {}
		~DD_UniquePtr()
		{
			if (m_pointer != nullptr)
				delete m_pointer;
		}

		// 복사 생성자 대입연산자 삭제
		DD_UniquePtr(const DD_UniquePtr& rhs) = delete;
		DD_UniquePtr& operator= (const DD_UniquePtr& rhs) = delete;

		DD_UniquePtr(DD_UniquePtr&& rhs) noexcept
		{
			this->m_pointer = rhs.m_pointer;
			rhs.m_pointer = nullptr;
			std::cout << "Move Constructor Called" << std::endl;
		}
		DD_UniquePtr& operator= (DD_UniquePtr&& rhs) noexcept
		{
			if (this != &rhs)
			{
				delete m_pointer;
				m_pointer = rhs.m_pointer;
				rhs.m_pointer = nullptr;
			}

			std::cout << "Move Assignment Operator Called" << std::endl;
			return *this;
		}
	};

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

SharedPtr – 여러 곳에서 쓰는 경우

unique_ptr처럼 “혼자만 쓰는 자원”이 아닐때 사용

  • 여러 시스템이 같은 객체를 참조
  • 생존 시점을 명확히 정하기 어려운 경우

이럴 때는 shared_ptr이 필요해진다.


참조 카운트 Control Block 하나로 해결

  • 실제 객체 포인터
  • refCount를 가진 ControlBlock
	struct ControlBlock
	{
		int refCount = 1;
	};

복사될 때 증가
소멸될 때 감소
0이 되면 자원 해제

	void Release()
	{
		if (m_control)
		{
			--m_control->refCount;

			if (m_control->refCount == 0)
			{
				delete m_pointer;
				delete m_control;
			}
		}

		m_pointer = nullptr;
		m_control = nullptr;
	}

참조 카운트로 누가 이 메모리를 참조하고 있는지 카운팅한다.
순환 참조 문제는 있긴한데 weak_ptr 과 함께 나중에 다루도록..하겠다. 

더보기
#pragma once
#include <iostream>

namespace DD
{
	template <class T>
	class DD_SharedPtr
	{
	private:
		struct ControlBlock
		{
			int refCount = 1;
		};

		T* m_pointer = nullptr;
		ControlBlock* m_control = nullptr;

	public:
		DD_SharedPtr() {}
		explicit DD_SharedPtr(T* ptr)
			: m_pointer(ptr)
		{
			if (ptr)
				m_control = new ControlBlock();
		}
		~DD_SharedPtr()
		{
			Release();
		}

		DD_SharedPtr(const DD_SharedPtr& rhs)
		{
			m_pointer = rhs.m_pointer;
			m_control = rhs.m_control;

			if (m_control)
				++m_control->refCount;

			std::cout << "Copy Constructor Called (refCount = "
				<< use_count() << ")\n";
		}

		DD_SharedPtr& operator=(const DD_SharedPtr& rhs)
		{
			if (this != &rhs)
			{
				Release();

				m_pointer = rhs.m_pointer;
				m_control = rhs.m_control;

				if (m_control)
					++m_control->refCount;
			}

			std::cout << "Copy Assignment Operator Called (refCount = "
				<< use_count() << ")\n";
			return *this;
		}

		DD_SharedPtr(DD_SharedPtr&& rhs) noexcept
		{
			m_pointer = rhs.m_pointer;
			m_control = rhs.m_control;

			rhs.m_pointer = nullptr;
			rhs.m_control = nullptr;

			std::cout << "Move Constructor Called\n";
		}

		DD_SharedPtr& operator=(DD_SharedPtr&& rhs) noexcept
		{
			if (this != &rhs)
			{
				Release();

				m_pointer = rhs.m_pointer;
				m_control = rhs.m_control;

				rhs.m_pointer = nullptr;
				rhs.m_control = nullptr;
			}

			std::cout << "Move Assignment Operator Called\n";
			return *this;
		}

	public:
		void reset(T* ptr = nullptr)
		{
			Release();

			if (ptr)
			{
				m_pointer = ptr;
				m_control = new ControlBlock();
			}
		}

		int use_count() const
		{
			return m_control ? m_control->refCount : 0;
		}

		T* get() const { return m_pointer; }
		T& operator*() const { return *m_pointer; }
		T* operator->() const { return m_pointer; }

	private:
		void Release()
		{
			if (m_control)
			{
				--m_control->refCount;

				if (m_control->refCount == 0)
				{
					delete m_pointer;
					delete m_control;
				}
			}

			m_pointer = nullptr;
			m_control = nullptr;
		}
	};

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

 

RAII를 머리로만 이해할 때는 “자동으로 정리해준다” 정도로 생각했는데,
스마트 포인터를 직접 구현해보니까 되게 단순한 구조로 되어있다는 생각이 든다.

  • 생성자 = 자원 획득
  • 소멸자 = 자원 해제

정리

  • RAII는 “자원을 객체 생명주기에 묶자”는 원칙
  • UniquePtr → 단일 소유
  • SharedPtr → 다중 소유 + 참조 카운트
  • 스마트 포인터는 RAII의 가장 대표적인 구현
COMMENT