C++ - Smart Pointer

mohadang·2022년 10월 14일
0

C++

목록 보기
27/48
post-thumbnail
post-custom-banner

Smart Pointer

  • new 사용하자 마자 delete도 같이 적는것이 좋은 습관
{
  Vector* myVector = new Vector(10.f, 30.f);

  // ==> LOGIC

  delete myVector
}
  • 더 이상 포인터가 필요하지 않을 때 메모리를 해제해야 함
  • 메모리 직접 해제는 가비지 컬렉션보다 빠르다.
  • 하지만 위의 코드만으로 개발자의 실수를 방지 하는데는 한계가 있음
  • 새로운 방식 필요
    • 스마트 포인터

unique_ptr

  • 소유자가 한명 밖에 없도록 강제함.
  • Ex)
#include <memory>

std::unique_ptr<Vector> myVector(new Vector(10.f, 30.f));// 포인터 부호(*)가 없음

myVector->Print();// 허나 포인터처럼 동작

return 0;// delete가 없음.

// unique_ptr 소멸자에서 new Vector 해제해줌.
  • 포인터(원시 포인터라 부르자)를 단독으로 소유
  • 원시(naked) 포인터는 누구하고도 공유되지 않음
  • 따라서 복사나 대입 불가
  • "unique_ptr가 범위(scope)를 벗어날 때, 원시 포인터는 지워짐(delete), 소멸자 호출"
  • 다음과 같은 구문은 Error이다.
std::unique_ptr<Vector> copiedVector1 = myVector;
std::unique_ptr<Vector> copiedVector2(myVector);

유니크 포인터를 사용하면 좋은 Case

  • Case1 : 클래스에서 생성자/소멸자

    • old
    class Player
    {
    private:
      Vector* mLocation;
    };
    
    Player::Player(std::string name)
      : mLocation(new Vector())
    {
    
    }
    
    Player::~Player() // 소멸자 필요...
    {
      delete mLocation;
    }
    • unique_ptr
    class Player
    {
    private:
      std::unique_ptr<Vector> mLocation;
    };
    
    Player::Player(std::string name)
      : mLocation(new Vector())
    {
    
    }
    
    // 소멸자에 delete 키워드 없음.
  • Case2 : 지역 변수

    • old
    #include "Vector.h"
    
    int main()
    {
      Vector* vector = new Vector(10.f, 30.f);
    
      vector->Print();
    
      delete vector;
    }
    • unique_ptr
    #include <memory>
    ##include "Vector.h"
    
    int main()
    {
      std::unique_ptr<Vector> vector(new Vector(10.f, 30.f));
    }
    • 주로 지역변수를 스택에 할당해서 사용 하였다, 그러나 가끔 스택에 할 당 못 하는 경우도 있다.
    • 이유는 스택에 엉청난 오브젝트를 만들면 느려 질 수도 있고 스택 오버 플로우도 일어 날 수 있다.
  • Case3 : STL 벡터에 포인터 저장하기

    • old
    #include <vector>
    #include "Player.h"
    
    int main()
    {
      std::vector<Player*> players;
    
      players.push_back(new Player("Lulu"));
      players.push_back(new Player("Coco"));
    
      for(int i = 0; i < players.size(); ++i) // 모두 지워주는 처리 필요
      {
        delete players[i];
      }
    
      players.clear();
    }
    • unique_ptr
    #include <vector>
    #include "Player.h"
    
    int main()
    {
      std::vector<std::unique_ptr<Player>> playersList;
    
      players.push_back(std::unique_ptr<Player>(new Player("Lulu")));
      players.push_back(std::unique_ptr<Player>(new Player("Coco")));
    
      players.clear();
    }
    
    • Stack에 오브젝트 할 당 하자니 너무 클 때

"uniqie 포인터의 문제점"

  • 원시 포인터 공유
Vector* vectorPtr = new Vector(10.f, 30.f); // 0x40000000
std::unique_ptr<Vector> vector(vecgtorPtr);
  // ptr = 0x40000000
std::unique_ptr<Vector> anotherVector(vectorPtr);
  // ptr = 0x40000000 문제 발생 더 이상 unique 하게 메모리를 가리키고 있지 않는다.

// 연산자 오버로딩, anotherVector의 ptr을 null로 초기화, 
// 그리고 소멸자 호출해서 메모리 해제
anotherVector = nullptr;

// "여기서 스코프 빠져나오면 vector와 anotherVector가 소멸자 호출하면서 
// 해제된 메모리를 또 해제하는 참사가 발생..."
  • C++14 이후 해결책
#include <memory>
#include "Vector.h"

int main()
{
  std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f, 30.f);
    // make_unique가 알아서 new Vector(10.f, 30.f)
    // make_unique를 사용하면 Naked pointer (new Vector(10.f, 30.f)) 
    // 를 참조 할 방법이 없기에 공유 문제 해결 할 수 있음.
}
  • unique pointer를 사용하고 싶다면 언제나 make_unique를 쓰자.

make_unique

  • 주어진 매개변수와 자료형으로 new 키워드를 호출해 줌
  • 따라서 원시 포인터와 같음
  • 둘 이상의 std::unique_ptr가 원시 포인터를 공유할 수 없도록 막는게 전부
  • Ex) 컴파일 에러
Vector* vectorPtr = new Vector(10.f, 30.f);

std::unique_ptr<Vector> vector1 = 
  std::make_unique(vectorPtr);// error
std::unique_ptr<Vector> vector2 = 
  std::make_unique<Vector>(vectorPtr);// error
std::unique_ptr<Vector> vector3 = 
std::make_unique<Vector*>(10.f, 30.f);// error

유니크 포인터 만들기

  • non-array
template<class T, class... Args> // 가변 인자.
unique_ptr<T> make_unique(Args&&... args);// r-value
  • array
template<class T>
unique_ptr<T> make_unique<std::size_t size);
  • C++11 (C++14를 쓸 수 없는 경우)
std::unique_ptr<Vector> vector(new Vector(10.f, 30.f));
std::unique_ptr<Vector[]> vectors(new Vector[20]);
  • C++14 (이걸 쓰자)
std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
std::unique_Ptr<Vector[]> vectors = std::make_unique<Vector[]>(20);

reset()

std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);

// <Vector>(10.f, 30.f) 해제함
// unique pointer가 new Vector(20.f, 40.f)를 새로 가리키게 함.
vector.reset(new Vector(20.f, 40.f));

// nullptr를 대입함
// 메모리 해제함
vector.reset();
  • vector.reset(); vs vector = nullptr;
    • 두 코드는 같음
    • nullptr가 reset() 보다 가독성이 더 높음
    • 허나 reset()은 vector가 원시 포인터가 아님을 분명하게 보여줌.
    • 무었을 선택해도 상관 없음, 회사 방침에 따라...
    • 포인터를 교체한다
    • std::unique_ptr가 재설정될 때, 소유하고 있던 원시 포인터는 자동으로 소멸됨.

get() : 원시 포인터를 반환한다.

void Vector::Add(const Vector* other)
{
  // C++에서 메모리 소멸은 생성한 쪽에서 해제하는 것을 원칙으로 하니 
  // Naked 포인터를 받아서 소멸 시키지 않을 거라고 기대한다.
  // 내가 만들지 않은 라이브러리에서 Naked 포인터를 요청할 때 get()이 필요함.

  mX += other->mX;
  mY += other->mY;
}

// ...

#include <memory>
#include "Vector.h"

int main()
{
  std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
  std::unique_ptr<Vector> anotherVector = std::make_unique<Vector>(20.f, 40.f);

  vector->Add(anotherVector.get());
  vector->Print();
}

release() : 메모리 소유권 이전

  • 이거 사용하게 되면 code가 조금 복잡해 짐
  • 소유권이 이전되면 지우는건 누구 책임임 ??
  • 그래서 좋은 생각은 아니다...
    • 포인터 소유권 박탈하기
  • 유니크 포인터를 복사할 수 있나 ???
    • No
  • 그렇다면 소유권 이전은
    • Yes, 소유권을 다른 std::unique_ptr로 옮길 수 만 있음
int main()
{
  std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
  Vector* vectorPtr = vector.release();
  // vector에 있던 <Vector>(10.f, 30.f)가 vectorPtr로 이전됨. 
  // 소멸자 호출 X, vector는 empty가 됨
}

소유권 이전하기

  • std::unique_ptr는 소유한 원시 포인터를 아무하고도 공유하지 않음
  • 즉, 주소 복사를 하지 않는다는 뜻
  • 대신, 소유권을 다른 std::unique_ptr로 옮길 수 있음
  • 예외: const std::unique_ptr

std::move()

  • 개체 A의 모든 멤버를 포기하고 그 소유권을 B에게 주는 방법
  • 메모리 할당과 해제가 일어나지 않음
  • 간단하게, A에 있는 모든 포인터를 B에게 대입하고 A에는 nullptr를 넣는다고 생각하자
    • 값이라면 값 복사함
    • 함수 구현 해야함.
  • "나는 멤버 변수를 옮기고 있다(MOVING)"
  • 이게 어떻게 도는지 알려면 r-value와 이동(move) 생성자를 배워야 됨

유니크 포인터 소유권 옮기기

##include <memory>
#include "Vector.h"

int main()
{
  std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
  std::unique_ptr<Vector> anotherVector(std::move(vector));
}

const 유니크 포인터 옮기기

const std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
std::unique_ptr<Vector> anotherVector(std::move(vector));

// 컴파일 에러 발생.
// const 니까 unique_ptr 개체 안에 멤버를 수정 할 수 없음.

STL 벡터에 요소 추가 할 때

std::vector<std::unique_Ptr<Player>> players;
std::unique_ptr<Player> coco = std::make_unique<Player>("Coco");
players.push_back(std::move(coco));
std::unique_ptr<Player> lulu = std::make_unique<Player>("Lulu");
players.push_back(std::move(lulu));

// push_back 할 때 move로 옮겨져야 하는 이유는 
// 'coco와 players 소멸될 때 소멸한 메모리를 중복해서 소멸하는'
// '사고를 방지하기 위해서'

std::unique_ptr의 내부

template<typename T>
class unique_ptr<T> final
{
public:
  unique_ptr(T* ptr) : mPtr(ptr) {}
  ~unique_Ptr(){delete mPtr;}
  T* get(){return mPtr;}
  unique_ptr(const unique_ptr&) = delete;
  unique_ptr& operator=(const unique_ptr&) = delete;
private:
  T* mPTr = nullptr;
};

베스트 프랙티스

  • 이제 다들 이걸 씀
  • 직접 메모리 관리하는 것만큼 빠름
  • RAII(Resource Acquisition Is Initialization) 원칙에 잘 들어맞음
    • 자원 할당은 개체의 수멸과 관련되어 있음
      • 메모리를 누가 소멸할지 애매한 상황을 피 할 수 있음.
    • 생성자에서 new 그리고 소멸자에서 delete
    • std::unique_ptr 멤버 변수가 이걸 해줌
    • 언제나 생성한 곳에서 삭제해야 한다.
  • 실수하기 어려움
  • 모든 곳에 쓰자!

shared_ptr

  • 메모리 소유권을 공유함

  • 참조 카운팅

  • 새로운 개념은 아님, 몇몇 업계에서는 사용하고 있었음...

  • 자동 메모리 관리

    • 주로 쓰는 두 가지 기법이 있음

      • 가비지 컬렉션(Garbage Collection, GC), java와 C#에서 지원

        • 종류가 많음
        • 메모리 릭 있음...
      • 참조 카운팅(Reference Counting, RefCounting), Swift와 애플 Objective-C에서 지원

        • 언어로 구현 가능.
        • Objective C에 ARC(Automatic Reference Counting) 옵션 있었음
        • 업계에서 직접 만들어 사용하는 경우 있음.
  • 가비지 컬렉션

    • 보통 트레이싱 가비지 컬렉션(Tracing Garbage Collection)d을 의미

    • 메모리 누수를 막으려는 시도

    • 주기적으로 컬렉션 실행

    • 충분한 여유 메모리가 없을 때 컬렉션이 실행됨

      • 스케쥴에 따라 또는 수동으로도 실행 가능
    • 매 주기마다, GC는 루트("root")를 확인함

      • 전역 변수
      • 스택
      • 레지스터
    • 힙에 있는 개체에 루트를 통해 접근할 수 있는지 판단

    • 접근할 수 없다면, 가비지로 간주해서 해제

    • Season GC : MS 사가 Garbaget Collection을 최적화 한 역사... 검색

  • 가비지 컬렉션의 문제점

    • 사용되지 않는 메모리를 즉시 정리하지 않음

    • GC가 메모리를 해제해야 하는지 판단하는 동안 애플리케이션이 멈추거나 버벅일 수 있음.

    • 30fps로 출력되는 게임이 있다고 하면 GC가 일어나면 그 시점에 프레임이 잠시 게임이 멈추는 것 처럼 보임,
      결과적으로 게임이 버벅거리는 현상(안드로이드 게임)

    • 아이폰은 GC 기반이 아님

      • 가비지가 생길 때 마다 지움. 그래서 30fps에 균일하게 멈춤을 배분하기에 버벅거리는 현상이 덜 함.
    • 따라서 스무스하게 보여줘야 하는 프로그램에서는 GC가 약점임.

    • 그러나 Office 같은 프로그램에서는 30fps으로 보여줄 필요도 없고 잠깐 멈춘다고 버벅거리지도 않기 때문 상관 없음.

      • C# 자체는 모든 프레임을 보여주는 프로그램에서 사용할 필요 없었다
    • C#에서 Garbage를 안 만들기 위해 어떻게 동작 하였나

      • 개체 만들고 안지우고 배열에 짱 밖아 두고 사용 할 때 쯤에 재 사용함.
  • 참조 카운팅(자동)

    • 만들어 사용 할 수 있음.(대입 연산자, 복사 생성자만 구현하면 됨... 그럼 어디서 사용하는지 알 수 있으니까.)
    • 가비지 컬렉션처럼, 개체에 대한 참조가 없을 때 개체가 해제됨
    • 언제든 참조 횟수를 활용해서 특정 개체가 몇 번이나 참조되고 있는지 판단 가능
    • 어떤 개체 A를 다른 개체 B가 참조할 때 횟수가 늘어남
    • B가 참조를 그만둘 때 횟수 줄어듦
      예) B가 범위(scope)를 벗어나는 경우
  • 수동 참조 카운팅
int main()
{
  Person* person = new Person("Coco");
  Person* copiedPerson = person;
  copiedPerson->AddRef();

  person->Release();
  person = nullptr;

  copiedPerson->Release();
  copiedPerson = nullptr;
}

class Person
{
private:
  // 소멸자를 숨겨야 되는 몇 안되는 경우.
  // 이렇게 만들면 Person 개체는 스택에 생성 못하고 힙에만 생성할 수 밖에 없다.
  // 스택에 생성하면 범위 벗어나는 순간 무조건 소멸자를 호출하니 컴파일 못함.
  ~Person() 
}

unsigned int Person::AddRef()
{
  ++mRefCount;
  return mRefCount;
}

unsigned int Person::Release()
{
  --mRefCount;
  if(mRefCount == 0)
  {
    delete this;
  }

  return mRefCount;
}

Person::Person(const std::string& name)
  : mRefCount(1)
  , mname(name)
{
}
Person::~Person()
{
}

// COM(DirectX)이 수동 참조 카운팅을 지원
// std::shared_ptr는 이걸 자동으로 해 줌.
  • 강한(Strong) 참조(지금까지 설명한게 강한 참조, 즉 일반적으로 참조하면 강한 참조를 말함.)

    • 강한 참조란 개체 A가 개체 B를 참조할 때, 개체 B는 절대 소멸되지 않음을 의미
      A* a = new A();
      a->b.Print();
    • 강한 참조의 수를 저장하기 위해 강한 참조 카운트를 사용
    • 일반적으로 새 인스턴스, 즉 개체에 대한 참조를 만들 때 강한 참조 횟수가 늘어남
    • 강한 참조 횟수가 0이 될 때 해당 개체는 소멸됨.
    • shared pointer는 강한 참조를 증가 시키는 방법
  • 참조 카운팅의 문제점

    • 참조 횟수는 너무 자주 바뀜
      • 멀티 쓰레드 환경에서 안전하려면, lock이나 원자적(atomic) 연산이 필요
        • Race condition
        • 원자적 연산이란 CPU 자체에서 Atomic 연산 지원하는 경우 있음(읽어서 쓰는것 까지 하나의 연산으로 지원) 그러나 역시 느림
      • ++mRefCount보다 확연히 느림
    • 순환(circular) 참조
      A* a = new A();
      a->b.Print();
      a->b.a.Print();
      // 개체 A가 개체 B를 참조
      // 개체 B가 개체 A를 참조
      // 절대 해제되지 않음~
      // C++에 해결책이 있음.    
  • GC나 RefCount를 쓰면 메모리 누수가 없다?

    • 전통적인 메모리 누수는 없음
      • 즉 delete를 잊었다
      • 사람들은 자주 이런 실수를 저지름
      • 발견하면 고치기는 쉬움
    • 하지만 여전히 메모리 누수가 발생할 수 있음
      • 순환 참조
      • 이런 실수는 좀 덜 함
      • 발견한다고 해도, 고치기 쉽지 않음
      • 이런 방식으로 사고하는 훈련이 되어 있지 않으면 자신이 발생시킨 메모리 누수를 스스로 바로잡지 못해서 멍청해 보일 것임
  • 가비지 컬렉션 vs 참조 카운팅

    • 가비지 컬렉션
      • 사용하기 확실히 더 쉬움
      • 실시간 또는 고성능 프로그램에 적합하지 않음
        • 3분마다 2초씩 정지하는 영화를 보고 싶을까?
        • 안드로이드 게임보다 더 버벅거리는 아이폰 게임 봤나요?
    • 참조 카운팅
      • 여전히 사용하기 쉬움
      • 실시간 또는 고성능 프로그램에 적합
      • 멀티 스레드 환경에서는 순수한 포인터보다 훨씬 느림.

std::shared_ptr

#include <memory>
#include "Vector.h"

int main()
{
  std::shared_ptr<Vector> vector = std::make_shared<Vector>(10.f, 30.f);
}
  • 두 개의 포인터를 소유
    • 데이터(원시 포인터)를 가리키는 포인터
    • 제어 블록을 가리키는 포인터(참조 횟수 가리키는 데이터)
  • std::unique_ptr와 달리, 포인터를 다른 std::shared_ptr와 공유할 수 있음
  • 참조 카운팅 기반
  • 원시 포인터는 어떠한 std::shared_ptr에게도 참조되지 않을 때 소멸 됨
[데이터 ptr]----------------> 데이터
[제어 블록 ptr]----------------> 강한 참조 횟수
shared_ptr<T>          약한 참조 횟수
                    AAllocator, delete, ...etc
  • 공유 포인터라고도 함.

포인터 공유하기

int main()
{
  std::shared_ptr<Vector> vector = std::make_shared<Vector>(10.f, 30.f);
  // ptr : 0x40000000;
  // control block : 0x50000000

  std::shared_ptr<Vector> copiedVector = vector;
  // ptr : 0x40000000;
  // control block : 0x50000000

  // 데이터 ptr, 제어 블록 ptr 모두 공유 한다.
}
  • reset 포인터 재설정하기
int main()
{
  std::shared_ptr<Vector> vector = std::make_shared<Vector>(10.f, 30.f);
  std::shared_ptr<Vector> copiedVector = vector;
  vector.reset();// vector == nullptr, 참조 하고 있는 것을 비우겠다.
  // 참조 카운트는 1이 되고 copiedVector만 가짐
}
  • 원시 포인터를 해제한다.
  • 참조 카운트가 1 줄어듦
  • nullptr를 대입하는 것과 같음

참조 횟수 구하기

int main()
{
  std::shared_ptr<Vector> vector = std::make_shared<Vector>(10.f, 30.f);
  cout << vector.use_count() << endl;// 1

  std::shared_ptr<Vector> copiedVector = vector;
  cout << vector.use_count() << endl;// 2
  cout << copiedVector.use_count() << endl;// 2
}

// 원시 포인터를 참조하고 있는 shared_ptr의 개수 반환

스택의 소멸 순서

std::shared_ptr<Person> owner = std::make_shared<Person>("Pope");
std::shared_ptr<Pet> pet = std::make_shared<Pet>("Coco");

Pope is created
Coco is created
Coco is destroyed
Pope is destroyed

// * 스택의 메모리 구조대로 LIFO

순환 참조

class Person
{
public:
  void SetPet(const std::shared_ptr<Pet>& pet);
private:
  std::shared_ptr<Pet> mPet;
};

class Pet
{
public:
  void SetOwner(const std::shared_ptr<person>& owner);
private:
  std::shared_ptr<Person> mOwner;
};


std::shared_ptr<Person> owner = std::make_shared<Person>("Pope");
std::shared_ptr<Pet> pet = std::make_shared<Pet>("Coco");

owner->SetPet(pet);
pet->SetOwner(owner);

// Pope is created
// Coco is created
  • Owner는 Pet을 Pet은 Owner를 참조하기에 스코프를 나가도 소멸자가 호출 되지 않는다.

  • 어떻게 고침??

    • 한명한테만 참조하게 해서 그 Object로 시작한다??
      • 또 다른 스마트 포인터가 있다,
      • "Weak pointer"

weak_ptr

  • 참조를 넘길 때 데이터를 저장하는 용도가 아닌 단순히 넘기는 용도??
  • 약한 참조는 원시 포인터 해제에 영향을 끼치지 않음
  • 약한 참조 카운트는 약한 참조의 수를 저장하는 데 사용됨
  • 약한 참조로 참조되는 개체는 강한 참조 카운트가 0이 될 때 소멸됨
  • 순환 참조 문제의 해결책
#include <memory>
#include "Person.h"

int main()
{
  std::shared_ptr<Person> owner = std::make_shared<Person>("Pope");
  // - 약한 참조를 사용하기 위해서는 강한 참조를 반드시 사용해야 한다.
  // - 강한 참조 카운트 : 1
  // - 약한 참조 카운트 : 1 <-- 기본이 1이다.

  std::weak_ptr<Person> weakOwner = owner;
  // - 강한 참조 카운트 : 1
  // - 약한 참조 카운트 : 2
}
  • 공유 포인터(shared_ptr)에서 부터 약한 포인터를 만들 수 있다.
  • lock, 약한 포인터로 공유 포인터 만들기
std::shared_ptr<Person> owner = std::make_shared<person>("Lulu");
std::weak_ptr<Person> weakOwner = owner;
// - 강한 참조 카운트 : 1
// - 약한 참조 카운트 : 1
  • 약한 포인터로 안에 있는 Person 쓸려고 함
  • 근데 문제가 있음, 멀티 스레드 환경에서 약한 포인터가 쓰려고 하는 Person이 사용하려 할 때 지워져 있을 수 있음.
  • 그래서 사용할 수 있는 권한이 없음.
std::shared_ptr<Person> lockedOwner = weakOwner.lock(); 
// - 강한 참조 카운트 : 2
// - 약한 참조 카운트 : 1

// lockedOwner = weakOwner 그냥 대입하면 에러
  • 그래서 약한 참조에서 Person을 사용하려면 강한 참조로 바꿔서 사용 해야함.

  • Person의 다른 참조가 지워지더라도 lockedOwner가 참조하기 때문에 지워지지 않는다.

  • 약한 포인터로 개체 사용 할 수 없음, 사용하고 싶다면 공유 포인터로 만들어야 함.

  • 쓰레드 락 개념이랑 매우 비슷.

  • 공유 포인터가 존재하는지 확인

std::shared_ptr<Person> owner = std::make_shared<person>("Lulu");
std::weak_ptr<Person> weakOwner = owner;

auto ptr  = weakOwner.lock();
if(ptr == nullptr)
{

}
  • expired 이용하는 방법
// 해제가 되었으면 true 반환.

// 1)
std::shared_ptr<Person> owner = std::make_shared<person>("Lulu");
std::weak_ptr<Person> weakOwner = owner;
if(weakOwner.expired()) // false
{

}
// 2)
std::shared_ptr<Person> owner = std::make_shared<person>("Lulu");
std::weak_ptr<Person> weakOwner = owner;

owner  = nullptr

if(weakOwner.expired()) // true
{

}
  • lock을 안 쓰고 이걸 쓰면 될까??
    • No, 쓰레드 안전하지 못함.
    if(weakOwner.expired()) <-- false
    {
    }
    • 여기서 다른 쓰레드 끼어들어서 object 지우는 어떻게함??
    • 따라서 expired는 방금 죽이고 정말 죽었는지 재 확인 할 때만.. 그 이외에는 lock으로 걸고 확인

순환 참조 해결하기

class Person
{
public:
  void SetPet(const std::shared_ptr<Pet>& pet);
private:
  std::shared_ptr<Pet> mPet;
};

class Pet
{
public:
  void SetOwner(const std::shared_ptr<person>& owner);
private:
  std::weak_ptr<Person> mOwner; // 순환 참조로
};

// Pope is created
// Coco is created
// Coco is destroyed
// Pope is destroyed
  • 스마트 포인터와 이중 연결 리스트
[강한 참조로만 이루어졌을 때 리스트 소멸]
  [S1  ]<->[S2  ]<->[S2   ]<->[S1   ]

  [S0  ]   [S1  ]<->[S2   ]<->[S1   ]
        뒤에 얘들 지워지지 않음.

[prev를 약한 참조로 만들 었을 때 리스트 소멸]
  [S1, W0]<->[S1, W1]<->[S1, W1]<->[S1, W1]

  [S1, W0]   [S0, W1]<->[S1, W1]<->[S1, W1]
  [S1, W0]        [S0, W1]<->[S1, W1]
  [S1, W0]             [S0, W1]
  [S1, W0]

- 한 방향으로만 연결 되어 있는것과 마찬 가지
- 연결 된 방향으로 연쇄적으로 소멸 됨.
- "강한 참조를 없앤다는 의미는 강한 참조를 하고 있는 개체의 소멸자를 호출하는 것과 같은 의미한다."

shared_ptr, weak_ptr

  • shared_ptr

    • 강한 참조
    • 강한 참조 카운트를 늘림
    • 직접적으로 사용할 수 있음
    • 원시 포인터가 확실히 존재하기 때문
  • weak_ptr

    • 약한 참조
    • 약한 참조 카운트를 늘림
    • 직접적으로 사용할 수 없음
    • lock을 써서 std::shared_ptr가 여전히 존재하는지 확인해야 함.
  • shared와 weak는 프로그램의 흐름을 이상하게 만든다.

    • weak를 사용하면 항상 expired 되었는지 확인 해야함.
    • shared 를 사용하려면 code review 같은거로 순환 되는지 규칙을 정하고 해야함.
    • shared
profile
mohadang
post-custom-banner

0개의 댓글