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의 가장 대표적인 구현
'STUDY > C++' 카테고리의 다른 글
| C++로 HashMap 직접 구현해보기 (Chaining + Rehash) (0) | 2025.12.23 |
|---|---|
| C++ - 자동으로 컴파일러가 만들어주는 복사생성자, 복사 대입 연산자 사용 방지법 (0) | 2022.07.28 |
| C++ - 가변 인자 템플릿 (Variadic Template) (0) | 2022.03.21 |
| C++ - 캐스팅(형변환), static_cast, dynamic_cast, const_cast,reinterpret_cast (0) | 2022.03.18 |
| C++ - Template 템플릿, 동작과정, typename과 class 선언의 차이 (0) | 2022.03.16 |