C++ 스마트 포인터의 원리와 활용

스마트 포인터가 필요한 이유

C++에서 동적 메모리를 직접 관리하다 보면 예외 발생 시 delete를 놓쳐 메모리 누수가 생길 수 있습니다. 특히 복잡한 제어 흐름이나 예외 처리 과정에서는 자원 해제를 보장하기 어렵습니다. 이를 해결하기 위해 '자원 획득은 초기화다(Resource Acquisition Is Initialization, RAII)'라는 개념을 기반으로 한 스마트 포인터가 등장했습니다.

RAII: 자원 관리의 핵심 원칙

RAII는 객체의 수명 주기를 스택 기반 지역 변수에 의존함으로써 자원을 안전하게 관리하는 기법입니다. 생성자에서 자원을 할당하고, 소멸자에서 반드시 해제함으로써 예외 상황에서도 자원 누수를 방지할 수 있습니다.

template<typename T>
class StackPtr {
private:
    T* data;

public:
    explicit StackPtr(T* ptr) : data(ptr) {}

    ~StackPtr() {
        if (data) {
            std::cout << "자동 해제: " << data << std::endl;
            delete data;
        }
    }

    // 복사 금지를 위한 삭제 선언
    StackPtr(const StackPtr&) = delete;
    StackPtr& operator=(const StackPtr&) = delete;
};

포인터처럼 사용하기: 연산자 오버로딩

스마트 포인터는 일반 포인터와 유사하게 동작해야 하므로 역참조(*)와 멤버 접근(->) 연산자를 오버로딩해야 합니다.

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

이를 통해 다음과 같이 자연스럽게 사용할 수 있습니다:

StackPtr<int> num(new int(42));
std::cout << *num;  // 값 접근

복사와 공유 전략의 진화

1. auto_ptr (비권장)

오래된 C++ 표준에서 사용되던 방식으로, 복사 시 소유권을 이전합니다. 하지만 직관적이지 않아 현재는 폐기되었습니다.

2. unique_ptr: 독점 소유

하나의 스마트 포인터만이 자원을 소유하며, 복사는 불가능하지만 이동은 가능합니다. 기본적으로 모든 복사 생성자와 대입 연산자를 삭제합니다.

template<typename T>
class UniquePtr {
    T* resource;

public:
    UniquePtr(T* ptr = nullptr) : resource(ptr) {}
    ~UniquePtr() { delete resource; }

    // 복사 금지
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;

    // 이동 허용
    UniquePtr(UniquePtr&& other) noexcept : resource(other.resource) {
        other.resource = nullptr;
    }

    UniquePtr& operator=(UniquePtr&& other) noexcept {
        if (this != &other) {
            delete resource;
            resource = other.resource;
            other.resource = nullptr;
        }
        return *this;
    }

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

3. shared_ptr: 참조 카운팅 기반 공유

여러 포인터가 같은 자원을 공유할 수 있으며, 내부적으로 참조 횟수를 세어 마지막 소유자가 사라질 때 자원을 해제합니다. 멀티스레드 환경에서는 원자성 보장을 위해 뮤텍스를 사용합니다.

template<typename T>
class SharedPtr {
    T* obj;
    int* refCount;
    std::mutex* mtx;

    void incRef() {
        mtx->lock();
        ++(*refCount);
        mtx->unlock();
    }

    void decRef() {
        bool shouldDelete = false;
        mtx->lock();
        if (--(*refCount) == 0) shouldDelete = true;
        mtx->unlock();

        if (shouldDelete) {
            delete obj;
            delete refCount;
            delete mtx;
        }
    }

public:
    explicit SharedPtr(T* ptr = nullptr)
        : obj(ptr), refCount(new int(1)), mtx(new std::mutex) {}

    SharedPtr(const SharedPtr& other) 
        : obj(other.obj), refCount(other.refCount), mtx(other.mtx) {
        incRef();
    }

    SharedPtr& operator=(const SharedPtr& other) {
        if (obj != other.obj) {
            decRef();
            obj = other.obj;
            refCount = other.refCount;
            mtx = other.mtx;
            incRef();
        }
        return *this;
    }

    ~SharedPtr() { decRef(); }

    T* operator->() const { return obj; }
    T& operator*() const { return *obj; }
    int use_count() const { return *refCount; }
};

순환 참조 문제와 해결책: weak_ptr

shared_ptr는 양방향 연결 구조(예: 이중 연결 리스트)에서 순환 참조로 인해 메모리 누수가 발생할 수 있습니다. 두 노드가 서로를 가리키면 참조 카운트가 0이 되지 않아 소멸되지 않습니다.

struct Node {
    SharedPtr<Node> prev;
    SharedPtr<Node> next;
}; // 순환 참조 → 메모리 누수!

이 문제를 해결하기 위해 소유권 없이 참조만 하는 weak_ptr를 사용합니다. 이는 참조 카운트를 증가시키지 않으므로 순환을 끊을 수 있습니다.

template<typename T>
class WeakPtr {
    T* weakRef;

public:
    WeakPtr() : weakRef(nullptr) {}
    WeakPtr(const SharedPtr<T>& sp) : weakRef(sp.get()) {}

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

// 수정된 노드 정의
struct Node {
    WeakPtr<Node> prev;
    SharedPtr<Node> next;
}; // 참조 카운트 문제 해결

커스텀 삭제기: 다양한 자원 해제 방식 지원

힙 메모리 외에도 파일 핸들, 배열, 소켓 등 다양한 자원을 관리해야 할 수 있습니다. 이를 위해 삭제 로직을 사용자가 지정할 수 있는 커스텀 삭제기를 제공합니다.

template<typename T, typename Deleter = std::function<void(T*)>>
class SharedPtrWithDeleter {
    T* resource;
    int* count;
    Deleter del;

public:
    template<typename D>
    SharedPtrWithDeleter(T* ptr, D d) 
        : resource(ptr), count(new int(1)), del(d) {}

    ~SharedPtrWithDeleter() {
        if (--(*count) == 0) {
            del(resource);
            delete count;
        }
    }

    // 나머지 구현은 동일...
};

실제 사용 예시:

// 배열 해제
SharedPtrWithDeleter<int[]> arr(new int[100], [](int* p) {
    delete[] p;
});

// 파일 닫기
SharedPtrWithDeleter<FILE> file(fopen("data.txt", "r"), [](FILE* f) {
    if (f) fclose(f);
});

// 네트워크 소켓 (가정)
SharedPtrWithDeleter<SOCKET> sock(socket(AF_INET, SOCK_STREAM, 0), closesocket);

태그: smart pointers RAII shared_ptr unique_ptr weak_ptr

5월 23일 17:32에 게시됨