소유권 모델 먼저 정리

타입소유권 의미대표 용도
std::unique_ptr<T>단독 소유(복사 불가, 이동만 가능)명확한 단일 owner
std::shared_ptr<T>공동 소유(참조 카운트)생명주기를 여러 객체가 공유
std::weak_ptr<T>비소유 관찰자순환 참조 방지, 약한 참조

핵심 원칙

  • 가능한 한 unique_ptr를 기본값으로 사용합니다.
  • 공유 소유권이 정말 필요한 경우에만 shared_ptr을 선택합니다.
  • weak_ptr는 "살아 있으면 쓰고, 없으면 포기"하는 안전한 관찰자입니다.

수동 레퍼런스 카운트가 위험한 이유

전형적 패턴

class RefCountable {
public:
    void AddRef() { ++refCount_; }
    void ReleaseRef() {
        if (--refCount_ == 0) delete this;
    }
private:
    int refCount_ = 1;
};

멀티스레드 타이밍 버그

  • 포인터를 전달한 뒤 AddRef()를 호출하기 전에 다른 스레드가 ReleaseRef()를 호출할 수 있습니다.
  • 이 경우 객체가 삭제된 후 접근하는 use-after-free가 발생합니다.
  • refCount_atomic으로 바꿔도 생명주기 hand-off 타이밍 문제는 남습니다.

shared_ptr의 본질: 제어 블록

구조

  • shared_ptr은 객체와 별도로 제어 블록(control block) 을 둡니다.
  • 제어 블록에는 strong count(shared_ptr 수), weak count(weak_ptr 수), deleter 정보가 들어갑니다.
  • 복사 시 strong count가 원자적으로 증가하고, 마지막 strong count가 0이 되면 객체가 파괴됩니다.

스레드 안전 경계 (매우 중요)

  • 안전한 것: 서로 다른 shared_ptr 인스턴스가 같은 객체를 참조할 때 count 갱신
  • 안전하지 않은 것: 객체 T 내부 상태를 여러 스레드가 동시에 변경
  • 즉, shared_ptr은 "수명 관리"를 안전하게 할 뿐, 객체 데이터 동기화까지 자동으로 해주지는 않습니다.

생성/소멸 실무 규칙

make_shared 우선

auto p = std::make_shared<Player>(id, name);
  • 일반적으로 예외 안전성과 성능(할당 횟수 감소) 측면에서 유리합니다.
  • 커스텀 deleter나 특수 생성 정책이 필요할 때만 shared_ptr(new T)를 신중히 사용합니다.

unique_ptr에서 shared_ptr로 승격

std::unique_ptr<Player> up = std::make_unique<Player>();
std::shared_ptr<Player> sp = std::move(up);  // 소유권 이전
  • 소유권이 단일에서 공유로 바뀌는 지점을 코드로 명확히 표현할 수 있습니다.

전달 방식 선택 가이드

함수 인자의미사용 기준
std::shared_ptr<T> 값 전달소유권 공유(수명 연장)비동기 작업에 보관 필요
const std::shared_ptr<T>&참조만, count 증가 없음호출 중 잠깐 접근
T* / T&소유권 없음수명 보장이 외부에서 명확할 때
std::weak_ptr<T>살아 있으면 사용콜백/관찰자 패턴

실무 팁

  • 비동기 큐/스레드 풀에 작업을 넘길 때는 값 전달 shared_ptr가 흔히 안전합니다.
  • 동기 함수 내부 즉시 사용이라면 const shared_ptr& 또는 T&가 더 가볍습니다.

weak_ptr와 순환 참조

왜 필요한가

  • shared_ptr끼리 서로를 참조하면 strong count가 0이 되지 않아 메모리 누수가 납니다.
struct B;
struct A { std::shared_ptr<B> b; };
struct B { std::weak_ptr<A> a; };  // 한쪽을 weak로

안전한 사용법

if (auto sp = wp.lock()) {
    sp->DoWork();
} else {
    // 이미 소멸됨
}
  • lock() 결과를 즉시 검사해 수명 경쟁(race)을 안전하게 처리합니다.

shared_ptr(this) 금지와 enable_shared_from_this

잘못된 코드

std::shared_ptr<Knight> p(this);  // 위험
  • 이미 다른 제어 블록이 존재하면 제어 블록이 2개 생겨 이중 삭제가 발생할 수 있습니다.

올바른 패턴

class Knight : public std::enable_shared_from_this<Knight> {
public:
    std::shared_ptr<Knight> GetPtr() {
        return shared_from_this();
    }
};

추가 주의사항

  • 객체가 shared_ptr로 소유되기 전 생성자에서 shared_from_this()를 호출하면 예외가 날 수 있습니다.
  • 소멸자 근처에서 자기 자신을 다시 공유하려는 패턴도 피해야 합니다.

공유 포인터 자체를 공유 변수로 쓸 때

문제

  • 여러 스레드가 같은 shared_ptr 변수를 동시에 읽고/바꾸면 경쟁이 생길 수 있습니다.

해법

  • mutex로 보호하거나,
  • C++20 이상에서 std::atomic<std::shared_ptr<T>>를 사용해 원자적 load/store/exchange를 적용합니다.

오해 금지

  • "shared_ptr은 스레드 안전"이라는 말은 보통 제어 블록 count 갱신에 대한 이야기입니다.
  • shared_ptr 변수 자체와 객체 내부 상태까지 자동으로 안전해지는 것은 아닙니다.

강의 시 유의사항

강조 포인트

  • 스마트 포인터는 메모리 해제 자동화 도구이면서, 동시에 소유권 설계 도구입니다.
  • shared_ptr의 안전 범위를 정확히 구분해야 멀티스레드 버그를 줄일 수 있습니다.
  • 순환 참조와 shared_ptr(this)는 실무에서 반복되는 대표 실수입니다.

자주 하는 오해

오해바로잡기
shared_ptr이면 동시성 버그가 사라진다수명 관리는 도움되지만 객체 상태 동기화는 별도 필요
atomic<int> refCount면 수동 관리도 안전하다hand-off 타이밍 문제는 여전히 위험
shared_from_this()는 어디서나 호출 가능shared ownership 확립 전/소멸 경로에서는 위험

체크 질문 (스스로 답해보기)

  • shared_ptr이 보장하는 스레드 안전 범위는 정확히 어디까지인가?
  • shared_ptr(this)가 제어 블록 2개 문제를 만들 수 있는가?
  • 콜백/비동기 코드에서 weak_ptr + lock() 패턴이 필요한 이유는?

profile
李家네_공부방

0개의 댓글