🧠 개요

C++로 서버나 게임을 개발할 때, 객체의 생명주기를 수동으로 관리해야 하는 상황이 종종 발생합니다. 특히 멀티스레드 환경에서는 객체가 예기치 않게 삭제되어 댕글링 포인터(Dangling Pointer) 문제가 생길 수 있죠.

이런 문제를 해결하기 위해 Reference Counting(참조 카운팅) 기법을 사용합니다. 이는 객체가 몇 군데에서 참조되고 있는지를 추적하고, 참조 수가 0이 되었을 때 안전하게 객체를 삭제하는 방식입니다.

이번 포스트에서는 이를 직접 구현하고, TSharedPtr 이라는 스마트 포인터도 만들어 봅니다.


📌 1. RefCountable 클래스 구현

✅ 참조 수를 관리하는 기본 클래스

#pragma once
#include <atomic>

class RefCountable
{
public:
    RefCountable() : _refCount(1) {}  // 생성 시 기본 참조 1
    virtual ~RefCountable() {}

    int32 GetRefCount() { return _refCount; }

    int32 AddRef() { return ++_refCount; }

    int32 ReleaseRef()
    {
        int32 refCount = --_refCount;
        if (refCount == 0)
            delete this;
        return refCount;
    }

protected:
    std::atomic<int32> _refCount;
};
  • AddRef()로 참조 수 증가
  • ReleaseRef()로 참조 수 감소
  • 참조 수가 0이면 delete this로 자신을 삭제

📌 2. 문제점: 수동 관리의 위험

❌ 예시 코드: 삭제된 객체를 여전히 참조

class A
{
public:
    int v = 10;
};

class B
{
public:
    void SetRef(A* a) { _ref = a; }
    void DoSomething() { _ref->v += 1; }

private:
    A* _ref = nullptr;
};
A* a = new A();
B* b = new B();
b->SetRef(a);

delete a;

while (true)
{
    b->DoSomething();  // 위험! 이미 삭제된 메모리를 참조
}
  • delete a 이후에도 ba를 참조함 → 댕글링 포인터
  • 크래시가 안 나고 쓰레기 값에 접근할 수도 있어 디버깅이 어렵다.

✅ 3. TSharedPtr: 직접 만든 스마트 포인터

📦 객체의 생명주기를 자동으로 관리

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

    // 복사 생성자
    TSharedPtr(const TSharedPtr& rhs) { Set(rhs._ptr); }

    // 이동 생성자
    TSharedPtr(TSharedPtr&& rhs) { _ptr = rhs._ptr; rhs._ptr = nullptr; }

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

    ~TSharedPtr() { Release(); }

    // 복사 연산자
    TSharedPtr& operator=(const TSharedPtr& rhs)
    {
        if (_ptr != rhs._ptr)
        {
            Release();
            Set(rhs._ptr);
        }
        return *this;
    }

    // 이동 연산자
    TSharedPtr& operator=(TSharedPtr&& rhs)
    {
        Release();
        _ptr = rhs._ptr;
        rhs._ptr = nullptr;
        return *this;
    }

    // 편의 연산자
    bool operator==(const TSharedPtr& rhs) const { return _ptr == rhs._ptr; }
    bool operator!=(const TSharedPtr& rhs) const { return _ptr != rhs._ptr; }
    T* operator->() { return _ptr; }
    const T* operator->() const { return _ptr; }
    T* operator*() { return _ptr; }
    const T* operator*() const { return _ptr; }
    bool IsNull() { return _ptr == nullptr; }

private:
    void Set(T* ptr)
    {
        _ptr = ptr;
        if (_ptr)
            _ptr->AddRef();
    }

    void Release()
    {
        if (_ptr)
        {
            _ptr->ReleaseRef();
            _ptr = nullptr;
        }
    }

    T* _ptr = nullptr;
};
  • Set() 시 참조 수를 증가
  • Release() 시 참조 수 감소 후 필요 시 객체 삭제
  • 일반 포인터처럼 사용할 수 있도록 연산자 오버로딩

💥 4. 예제 코드로 RefCount 확인

🧪 실습 클래스: Wraith와 Missile

class Wraith : public RefCountable
{
public:
    int _hp = 150;
    int _posX = 0;
    int _posY = 0;
};
using WraithRef = TSharedPtr<Wraith>;

class Missile : public RefCountable
{
public:
    void SetTarget(WraithRef target)
    {
        _target = target;
    }

    bool Update()
    {
        if (_target == nullptr)
            return true;

        // TODO: 추적 로직
        if (_target->_hp == 0)
        {
            _target = nullptr;  // 스마트 포인터가 자동으로 소멸 관리
            return true;
        }
        return false;
    }

private:
    WraithRef _target = nullptr;
};
using MissileRef = TSharedPtr<Missile>;

🧪 Main 함수

int main()
{
    WraithRef wraith(new Wraith());
    wraith->ReleaseRef();  // 초기 2 -> 1로 조정

    MissileRef missile(new Missile());
    missile->ReleaseRef();

    missile->SetTarget(wraith);

    wraith->_hp = 0;
    wraith = nullptr;  // 남은 참조가 missile 하나 뿐

    while (true)
    {
        if (missile && missile->Update())
        {
            missile = nullptr;  // Update 종료 후 자동 소멸
        }
    }
}

📌 5. 순환 참조(Cyclic Reference)의 문제점

  • AB를 참조하고, BA를 참조하면 둘 다 참조 카운트가 0이 되지 않음 → 메모리 릭 발생
  • weak_ptr 또는 약한 참조 개념으로 해결해야 함

profile
李家네_공부방

0개의 댓글