본문 바로가기
C++/문법 정리

Chapter 07. 스마트 포인터

by GameStudy 2022. 5. 8.

7.1 unique_ptr

  7.1-1 원시 포인터

    Def) 원시 포인터(Naked Pointer)

      이전까지 사용해오던 C 스타일의 포인터.

      아무것도 없이 위험한 세상에 노출되어 있단 뜻.

 

    Note) 원시 포인터는 더이상 필요하지 않다면 메모리 해제 필수

MyVector* ptrVec = new MyVector(7.f, 7.f);

...

delete ptrVec;
ptrVec = nullptr;

 

    Note) 스마트 포인터는 delete를 직접 호출할 필요가 없음.

      그래서 스마트임. 그리고 가비지 컬렉션보다도 빠름.

      쓰이지 않는 순간 곧바로 지워주기 때문.

 

    Note) 스마트 포인터의 종류

      - unique_ptr: 매우매우 중요한 스마트 포인터.

          이를 이해하기 위한 문법을 하나 더 숙지하고 있어야 함.

      - shared_ptr: unique_ptr보단 덜 쓰임.

          weak_ptr과 함께 다님.

      - weak_ptr: 사용하려면 shared_ptr이 필요함.

          생각보다 많이 쓰이진 않음.

 

  7.1-2 unique_ptr

    Def) unique_ptr

      원시 포인터를 단독으로 소유함. 누구와도 공유하지 않음.

      즉, 소유자가 한 명이라서 unique_ptr.

      소유자가 한 명이라면, 그 사람이 소멸시키면 되니까 RAII 준수.

      그래서 복사나 대입이 불가능함.

      unique_ptr은 해당 스코프를 벗어날 때, 원시 포인터가 지워짐(delete)

      namespace std, memory 헤더 파일에 정의되어 있음.

 

    Ex07010201)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(float x, float y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    unique_ptr<MyVector> ptrVec1(new MyVector(7.f, 7.f));
    unique_ptr<MyVector> ptrVec2 = unique_ptr<MyVector>(new MyVector(7.f, 7.f)); // 위 아래 코드는 암시적이냐, 명시적이냐 차이.

    ptrVec1->Print();
    ptrVec2->Print();

    return 0;
}

 

    Note) Ex07010201는 unique_ptr 개체를 스택에 생성하는 예제.

      즉, main() 함수가 반환될 때 자동으로 소멸자가 호출되어서 delete됨.

      early return을 한다해도, 함수 스택 프레임이 반환되며 자동 delete됨.

 

    Note) 소유자가 한 명이라 복사나 대입이 불가능한 unique_ptr

unique_ptr<MyVector> ptrVec1(new Vector(1.f, 2.f));

unique_ptr<MyVector> copiedPtrVec1 = ptrVec1; // compile error.
unique_ptr<MyVector> copiedPtrVec2(ptrVec1);  // compile error.

복사 생성자 unique_ptr(const unique_ptr&)이 삭제되었다고 나옴.

 

    Note) unique_ptr은 다음 세 가지 경우에 적합함.

      1. 클래스의 멤버로 다른 클래스의 원시 포인터를 갖을 때

      2. 동적 할당된 원시 포인터의 경우

      3. STL 컨테이너에 원시 포인터를 저장할 때

 

    Ex07010202)

<hide/>

// main.cpp

#include <iostream>
#include <string>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

class Player
{
public:
    Player(void) = default;
    Player(const string& name, const float& x, const float& y)
        : mName(name)
    {
        mLocation = unique_ptr<MyVector>(new MyVector(x, y));
    }

    Player(const Player& other) = delete;

    virtual ~Player(void) {};

    void Print(void) { cout << mName << ": ";  mLocation->Print(); }
        
private:
    string mName;
    unique_ptr<MyVector> mLocation;
};

int main(void)
{
    Player me("Parkthy", 2.1f, 3.2f);

    me.Print();

    return 0;
}

 

    Ex07010203)

<hide/>

// main.cpp

#include <iostream>
#include <string>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    MyVector* pVec1 = new MyVector(1.f, 3.1f);

    pVec1->Print();

    delete pVec1;    // 원시 포인터이기에, delete는 필수적임. 안그럼 메모리 누수 발생.
    pVec1 = nullptr; // 해제된 원시 포인터의 재사용 문제도 있기에, nullptr 대입도 센스껏 필요함.

    unique_ptr<MyVector> pVec2(new MyVector(2.1f, 3.1f));

    pVec2->Print();
    
    // 메모리 누수 걱정과 해제된 원시 포인터 재사용 문제도 전혀 없음.

    return 0;
}

 

    Ex07010204)

<hide/>

// main.cpp

#include <iostream>
#include <string>
#include <vector>
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

class Player
{
public:
    Player(void) = default;
    Player(const string& name, const float& x, const float& y)
        : mName(name)
        , mLocation(new MyVector(x, y)) // 이렇게도 가능.
    {
        // mLocation(new MyVector(x, y)) compile error.
    }

    Player(const Player& other) = delete;

    virtual ~Player(void) {};

    void Print(void) { cout << mName << ": ";  mLocation->Print(); }
        
private:
    string mName;
    unique_ptr<MyVector> mLocation;
};

int main(void)
{
    _CrtDumpMemoryLeaks();
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

    vector<Player*> players;
    players.push_back(new Player("Parkthy", 1.1f, 1.2f));
    players.push_back(new Player("Kimthy", 2.1f, 2.2f));

    for (const auto& player : players) { player->Print(); }

    for (auto& player : players) 
    { 
        delete player;
        player = nullptr;
    }
    players.clear();

    vector<unique_ptr<Player>> players2;
    players2.push_back(unique_ptr<Player>(new Player("Parkthy", 1.1f, 1.2f)));
    players2.push_back(unique_ptr<Player>(new Player("Kimthy", 2.1f, 2.2f)));

    for (const auto& player : players2) { player->Print(); }

    return 0;
}

 

  7.1-3 make_unique<>()

    Note) 이렇게 완벽해 보이는 unique_ptr의 문제점.

      하나의 원시 포인터를 공유할 수 있음.

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    MyVector* nakedPtrVec = new MyVector(1.f, 2.f);

    unique_ptr<MyVector> pVec1(nakedPtrVec);
    unique_ptr<MyVector> pVec2 = unique_ptr<MyVector>(nakedPtrVec);

    pVec2 = nullptr; // 중단점 -> local 조사식 보면서 F11 눌러보기.
      // pVec2에 nullptr이 대입되는 순간, 원시 포인터의 소유권을 놓겠다는 의미. 소멸자 ~unique_ptr()가 자동 호출됨.
      // 그럼 pVec2는 empty가 되고, nakedPtrVec의 값은 날아가버림.
      // 근데 pVec1은 아직도 자신의 원시 포인터가 유효한 줄 앎. 갖고 있음.
      // 그러다가 소멸될 때 이미 지워진 걸 다시 지우려함. undefined behavior.
    int a = 10;

    delete nakedPtrVec;
    nakedPtrVec = nullptr;
      // 근데 unique_ptr은 대입 안된다더니?
      // unique_ptr 클래스에는 대입 연산자를 오버로딩한 reset() 메서드가 있음.
      // 이 reset() 메서드가 호출 된 것. 
      // 즉 '다른 포인터를 대입할 순 없다. 하지만 지금 가지고 있는 포인터를 포기할 순 있다.'라는 의미

    return 0;
}

 

    Def) make_unique<>();

      주어진 템플릿 자료형과 함수 인자로 new를 호출해줌.

      따라서 new 해서 원시 포인터 만드는 것과 똑같음.

      둘 이상의 unique_ptr가 원시 포인터를 공유할 수 없도록

      막는 것이 전부임. 보이지가 않으니 건들수가 없음.

      이제부터 new를 쓰기보다 make_unique라는 utility 함수를 쓰자.

      이렇게 쓰면 원시 포인터를 꺼내올 방법이 없어짐.

 

    Ex07010301)

// <hide/>

// main.cpp

#include <iostream>
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    _CrtDumpMemoryLeaks();
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

    unique_ptr<MyVector> pVec = make_unique<MyVector>(1.1f, 3.f);

    pVec->Print();

    return 0;
}

 

    Note) 따라서 아래와 같이 작성하면 컴파일 에러

MyVector* ptrVec = new MyVector(1.f, 3.f);

unique_ptr<MyVector> v1 = make_unique(ptrVec);              // complie error. 이렇게 원시포인터가 보이는 걸 막기위함인데..
unique_ptr<MyVector> v2 = make_unique<MyVector>(ptrVec);    // compile error. 마찬가지..
unique_ptr<MyVector> v3 = make_unique<MyVector*>(1.f, 3.f); // compile error. 이건 원래 *을 안붙혔음. 문법오류.

 

    Note) make_unique<>()의 구현 개요

// non-array
template< class T, class... Args >            // class...은 가변인자 템플릿 매개변수. 뒤에 배움.
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을 쓸 수 없는 경우)
unique_ptr<MyVector> vector(new MyVector(1.f, 2.f))
unique_ptr<MyVector> vectors(new MyVector[20]);

// C++14 (이게 베스트)
unique_ptr<MyVector> vector = make_unique<MyVector>(1.f, 2.f);
unique_ptr<MyVector[]> vectors = make_unique<MyVector[]>(20);

 

  7.1-4 reset()

    Def) void reset(pointer ptr = pointer()) noexcept;

      unique_ptr이 가지고 있던 원시포인터가 교체됨.

      이때 원래 소유하고 있던 원시 포인터는 자동으로 소멸됨.

unique_ptr<MyVector> pVec = make_unique<MyVector>(1.f, 2.f);
pVec.reset(new Vector(1.1f, 2.1f));
pVec.reset(); 
  // pVec = nullptr;와 똑같은 코드.
  // nullptr을 대입해주는 것이 가독성으로선 더 좋은듯.
  // 하지만, reset()을 쓰는게 pVec이 unique_ptr이라는 것을 분명히 보여줌.
  // 따라서 둘 다 선호도가 높음.

 

  7.1-5 get()

    Def) pointer get() const noexcept;

      원시 포인터를 반환함.

      unique_ptr에는 원시 포인터 말고도 다른 멤버도 있음.

      그 때문에 필요한 함수임. 

      다만 반환 받은 get() 함수 호출자가 말도 안되게

      바꿔버리거나 할 수 있지만, 그러지 않는다는 가정하에 사용.

unique_ptr<MyVector> v;
MyVector* ptr = v.get();

      

    Ex07010501)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };
    void PrintSum(const MyVector* nakedPtr) const { cout << nakedPtr->mX + nakedPtr->mY << endl; }

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    unique_ptr<MyVector> v = make_unique<MyVector>(2.f, 5.f);

    v->PrintSum(v.get());

    return 0;
}

 

  7.1-6 release()

    Def) pointer release() noexcept;

      해당 unique_ptr의 원시 포인터에 대한 소유권을 박탈하고

      원시 포인터를 반환함. 즉, 호출자에게 소유권을 넘겨주겠다는 것.

 

    Note) release()의 호출은 예전 방식으로의 회귀임.

      소유권이 자꾸 바뀔 수 있기 때문. 추천하는 방식이 아님.

 

    Note) release() 호출 후 get() 호출하면 nullptr이 반환됨.

unique_ptr<MyVector> v = make_unique<MyVector>(1.f, 2.f);

MyVector* nakedPtrVec = v.release();
MyVector* nullPointer = v.get();

 

    Ex07010601)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };
    void PrintSum(const MyVector* nakedPtr) const { cout << nakedPtr->mX + nakedPtr->mY << endl; }

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    unique_ptr<MyVector> v = make_unique<MyVector>(1.f, 2.f);
    MyVector* nakedPtrVec = v.release();
    nakedPtrVec->PrintSum(nakedPtrVec);

    MyVector* nullPointer = v.get();
    if (nullptr == nullPointer) { cout << "nullPointer == nullptr" << endl; }

    return 0;
}

 

  7.1-7 move()

    Def) move()

      unique_ptr 개체의 모든 멤버를 포기하고

      그 소유권을 다른 unique_ptr 개체에 주는 방법.

      메모리 할당과 해제가 일어나지 않음.

      이전에 배운 swap()과 비슷함. 

      이전 unique_ptr 개체의 모든 포인터를

      다른 unique_ptr 개체에 대입하고 이전 개체에는 nullptr 대입.

 

    Note) move()는 release()보다 비교적 안전함.

      release()는 원시 포인터를 뽑아내기 때문에 위험.

 

    Note) "나는 멤버 변수를 옮기고 있다"

      이를 Moving이라 하고, 이게 어떻게 도는지 알려면

      r-value와 이동 연산자(move)를 배워야함. 이후에 배움.

 

    Ex07010701)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };
    void PrintSum(const MyVector* nakedPtr) const { cout << nakedPtr->mX + nakedPtr->mY << endl; }

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    unique_ptr<MyVector> pV1 = make_unique<MyVector>(2.f, 3.f);
    unique_ptr<MyVector> pV2(move(pV1));
    if (nullptr == pV1.get()) { cout << "move() has been called successfully." << endl; }

    pV2->Print();

    const unique_ptr<MyVector> pV3 = make_unique<MyVector>(5.f, 4.f);
    unique_ptr<MyVector> pV4(move(pV3));

    return 0;
}

 

    Ex07010702)

<hide/>

// main.cpp

#include <iostream>
#include <vector>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    void Print(void) const { cout << mX << ", " << mY << endl; };
    void PrintSum(const MyVector* nakedPtr) const { cout << nakedPtr->mX + nakedPtr->mY << endl; }

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

class Player
{
public:
    Player(void) = default;
    Player(const string& name, const float& x, const float& y)
        : mName(name)
        , mLocation(new MyVector(x, y)) // 이렇게도 가능.
    {
        // mLocation(new MyVector(x, y)) compile error.
    }

    Player(const Player& other) = delete;

    virtual ~Player(void) {};

    void Print(void) { cout << mName << ": ";  mLocation->Print(); }
        
private:
    string mName;
    unique_ptr<MyVector> mLocation;
};

int main(void)
{
    vector<unique_ptr<Player>> players;

    unique_ptr<Player> p = make_unique<Player>("Park", 1.f, 2.f);
    players.push_back(move(p));

    p = make_unique<Player>("Kim", 6.f, 5.f);
    players.push_back(move(p));

    for (const auto& player : players) { player->Print(); }

    return 0;
}

 

  7.1-8 unique_ptr 베스트 프렉티스

    Note) unique_ptr의 내부 구현

// 헤더파일에 공개되어 있음. 쓸데없는거 지우고 필요한 거만 있는 코드.

template< typename T >
class unique_ptr<T> final
{
public:
    unique_ptr(T* ptr)
    	: mPtr(ptr)
    {}
     
    ~unique_ptr(void) { delete mPtr; }                 // mPtr이 nullptr이라면? C++ 스팩상, delete nullptr;은 아무일도 안함.
      
    T* get(void) { return mPtr; }
    unique_ptr(const unique_ptr&) = delete;            // 복사생성자와 대입생성자 지워버림.
    unique_ptr& operator=(const unique_ptr&) = delete;
    
private:
    T* mPtr = nullptr;                                 // T만 줘도 알아서 *을 붙혀줌.
}

// 이런식으로 도는거 아주 많음.
// ex. multithreading에서도 이런 구조를 활용해서 deadrock을 막음.

 

    Note) unique_ptr 베스트 프렉티스

      1. 다들 unique_ptr을 씀.

        직접 메모리 관리하는 것만큼 빠름.

        RAII 원칙에 잘 들어맞음.

        자원 할당은 개체의 수명과 관련되어 있음.

        생성자에서 new 그리고 소멸자에서 delete

        unique_ptr 멤버가 이걸 해줌.

      2. 실수하기가 어려움. 따라서 모든 곳에 쓰는 것을 추천.

 

    Ex07010801)

<hide/>

// main.cpp

#include <iostream>
#include <vector>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    float GetX(void) const { return mX; }
    float GetY(void) const { return mY; }

    virtual ~MyVector(void) {};

private:
    float mX;
    float mY;
};

int main(void)
{
    unique_ptr<int> num1 = make_unique<int>(10);
    unique_ptr<char> char1 = make_unique<char>('d');
    cout << *num1 << endl;
    cout << *char1 << endl;
    unique_ptr<MyVector> myVector = make_unique<MyVector>(3, 5);
    cout << "X: " << myVector->GetX() << ", Y: " << myVector->GetY() << endl;
    // Compiler error
    // unique_ptr<MyVector> copyOfMyVector1 = myVector;
    // unique_ptr<MyVector> copyOfMyVector2(myVector);
    myVector.reset(new MyVector(1, 5));
    cout << "X: " << myVector->GetX() << ", Y: " << myVector->GetY() << endl;
    num1.reset();
    myVector = nullptr;

    unique_ptr<char> char2(move(char1));
    // Run-time error
    //cout << "Char1: " << *char1 << endl;
    cout << "Char2: " << *char2 << endl;
    const unique_ptr<float> float1 = make_unique<float>(2.0f);
    // Compile error
    // unique_ptr<float> float2(move(float1));
    //float1.reset(new float(1.0f));

    return 0;
}

 

 

7.2 shared_ptr

  7.2-1 자동 메모리 관리

    Note) shared_ptr는 소유권을 공유하는 스마트 포인터

      unique_ptr은 소유권을 한 명만 갖고 있기 때문에

      원시 포인터를 누가 소멸 시켜줘야하는지 분명했음.

      그럼 shared_ptr은 누가 원시 포인터를 소멸시켜주지?

      

    Note) 자동 메모리 관리 방법

      1. 가비지 컬렉션(Garbage Collection, GC)

        Java와 C#에서 지원함.

        여기서 Garbage는 더이상 사용되지 않는 개체를 의미.

      2. 참조 카운팅(Reference Counting, RefCounting)

        Swift와 애플 Objective-C에서 지원함.

        MS 사의 그래픽 라이브러리인 DirectX도 참조 카운팅 기반.

        단 수동 참조 카운팅임.

 

    Note) shared_ptr은 자동 참조 카운팅에서 시작되었음.

      unique_ptr도 자동관리가 되긴 했지만,

      소유권을 누군가에게 넘겨줄 때에 조금 문제가 있었음.

      물론 코딩 표준으로 잡을 수 있지만, 100% 신뢰는 못함.

      그래서 shared_ptr에서부터 진정한 자동 메모리 관리가 되는 것.

 

  7.2-2 가비지 컬렉션

    Note) 트레이싱 가비지 컬렉션(Tracing Garbage Collection)

      GC 기법 중 대표적인 기법. 메모리 누수를 막으려는 시도임.

      주기적으로 컬렉션을 실행함. 

      개체가 더이상 안쓰이는 순간에 바로 실행되는 것이 아님.

      길가에 누가 쓰레기 버린다해서 바로 주워가지 않듯이,

      충분한 여유 메모리가 없을 때 혹은 특정 경우에 실행됨.

 

    Note) 매 주기마다 GC는 루트(root)를 확인함.

      전역변수 / 스택 메모리 / 레지스터

      힙에 있는 개체에 루트를 통해 접근할 수 있는지 판단함.

      한 단계든 여러 단계든 접근 할 수 없다면,

      가비지로 간주해서 해제해버림.

 

    Note) 가비지 컬렉션의 개략적인 작동 방식

      루트 중 하나인 스택 메모리에 개체들이 있다고 해보자.

      개체들을 하나씩 훑어서 접근 가능한 힙 메모리들은 살려둠.

      그러나, 루트로부터 접근 불가능한 힙메모리들은 정리함.

 

    Note) seasonal GC

      위 키워드를 검색해보면, C#의 GC 최적화 방법을 엿볼 수 있음.

      요즘엔 Generation GC로 바뀐듯?

      지역 변수처럼 자주 생성되고 삭제되는 애들은 0세대

      그보다는 오래 살아남는 애들은 1세대 

      그보다도 더 오래 살아남는 애들은 2세대로 분류하고

      새대별로 가비지 컬렉팅해서 모든 메모리를 훑을 필요 없도록함.      

 

    Note) 가비지 컬렉션의 문제점

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

      메모리가 쌓여 있음.

      GC가 메모리를 해제해야 하는지 판단하는 동안(싱크해야 하는 순간)

      애플리케이션(게임, 실시간 앱 등)이 멈추거나 버벅일 수 있음.

      

    Note) 안드로이드 게임의 프레임 드랍

      안드로이드 OS는 GC 기법을 씀.

      그래서 버벅인다면 GC가 메모리를 정리하는 때일 가능성이 있음.

      IOS의 경우, RefCounting 기법을 채택 했기 때문에

      모든 프레임에 균일하게 개체를 지워나감.

 

  7.2-3 참조 카운팅

    Note) 참조 카운팅이란,

      특정 개체가 몇번이나 참조되고 있는지 판단함.

      즉, 어떤 개체 A가 다른 개체 B를 참조하면 횟수 증가.

      A가 참조를 그만둘 때 횟수 감소.

      ex. B가 스코프를 벗어나는 경우, ...

      그래서 결국 GC처럼 개체에 대한 참조 횟수가 없을 때

      개체가 해제됨.

      shared_ptr은 강한 참조와 약한 참조 개념 모두 가지고 있음.

 

    Def) 강한 참조(Strong Reference)

      개체 A가 개체 B를 참조하고 있다면

      개체 B가 절대로 소멸되지 않을 때 강한 참조라고 함.

      즉 지금까지의 참조 개념이 강한 참조였음.

      강한 참조의 수를 저장하기 위해 강한 참조 카운트를 사용

      일반적으로 새 개체에 대한 참조를 만들 때 강한 참조 횟수 증가.

      강한 참조 카운트가 0이 될 때, 해당 개체는 소멸됨.

 

    Note) 참조 카운팅의 문제점

      1. 참조 횟수가 너무 자주 바뀜.

        멀티 쓰레드 환경(Race condition)에서 안전하려면

        lock이나 원자적(atomic) 연산으로 참조 횟수를 관리해야함.

        근데 이는 ++mRefCount 하는 것처럼 빠르지 않음. 느림.

      2. 순환(circular) 참조

        개체 A가 개체 B를 참조, 개체 B가 개체 A를 참조

        그럼 A와 B는 이제 절대 지워지지 않음.

        이거도 하나의 메모리 누수임.

        곧 배우는 내용에서 해결책이 있음.

 

    Note) GC나 RefCount를 쓰면 전통적인 메모리 누수는 없음.

      즉, delete를 잊는 경우. 발견하면 고치기는 쉬움.

      그러나 여전히 메모리 누수가 발생할 수 있음. ex. 순환 참조.

      이런 실수는 좀 덜함. 발견한다해도, 고치기 쉽지 않음.

      그래서 이런 방식으로 사고하는 훈련이 되어 있지 않으면

      자신이 초래한 메모리 누수를 스스로 잡지 못함.

 

    Note) 가비지 컬렉션 Vs. 참조 카운팅

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

 

  7.3-4 shared_ptr

    Def) std::shared_ptr

      두 개의 포인터를 소유하고 있음.

      하나는 원시 포인터. 다른 하나는 제어 블록을 가리키는 포인터.

      제어 블록에는 강한 참조 횟수/약한 참조 횟수/Allocator, delete, ...가 있음.

 

    Note) shared_ptr의 특징

      1. unique_ptr과 달리, 원시 포인터를 다른 shared_ptr과 공유가능.

      2. 참조 카운팅 기반.

      3. 원시 포인터는 어떠한 shared_ptr에게도 참조되지 않을 때 소멸됨.

 

    Def) make_shared<>()

// non-array
template< class T, class... Args >
shared_ptr<T> make_shared(Args&&... args);

 

    Ex07030401)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    virtual ~MyVector(void) {}

    float GetX(void) const { return mX; }
    float GetY(void) const { return mY; }
    void Print(void) const { cout << mX << ", " << mY << endl; }

private:
    float mX;
    float mY;
};

int main(void)
{
    shared_ptr<MyVector> pVec = make_shared<MyVector>(1.f, 2.f);
    pVec->Print(); // 중단점 걸고 조사식 보면 알 수 있음.

    return 0;
}

 

    Note) 원시 포인터 공유하기

      대입 연산자를 통해서 공유 가능.

      참조 횟수도 똑같은 횟수로 공유하게 됨.

shared_ptr& operator=(const shared_ptr& other) noexcept;

template< class U >
shared_ptr& operator=(const shared_ptr<U>& other) noexcept;
shared_ptr<MyVector> pVec1 = make_shared<MyVector>(1.f, 2.f);
shared_ptr<MyVector> pVec2 = pVec1;

 

    Def) void reset() noexcept;

      원시 포인터를 해제함. 

      지운다는 뜻은 아님. nullptr을 원시포인터로 갖고 있겠다는 뜻.

      참조 카운트는 1이 줄어듬. 그냥 nullptr을 대입하는 것과 같음.

 

    Note) 원시 포인터 재설정하기

shared_ptr<MyVector> pVec1 = make_shared<MyVector>(1.f, 2.f);
shared_ptr<MyVector> pVec2 = pVec1;
pVec1.reset(); // pVec1 = nullptr;과 같음.

 

    Def) long use_count() const noexcept;

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

 

    Ex07030402)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class MyVector
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    virtual ~MyVector(void) {}

    float GetX(void) const { return mX; }
    float GetY(void) const { return mY; }
    void Print(void) const { cout << mX << ", " << mY << endl; }

private:
    float mX;
    float mY;
};

int main(void)
{
    shared_ptr<MyVector> pVec1 = make_shared<MyVector>(1.f, 2.f);
    cout << "RefCount of pVec1: " << pVec1.use_count() << endl;

    shared_ptr<MyVector> pVec2 = pVec1;
    cout << "RefCount of pVec1: " << pVec1.use_count() << endl;
    cout << "RefCount of pVec2: " << pVec2.use_count() << endl;

    return 0;
}

 

    Ex07030403)

<hide/>

// main.cpp

#include <iostream>
#include <string>

using namespace std;

class Pet final
{
public:
    Pet() = default;
    Pet(const string& name)
        : mName(name)
    {}

    Pet(const Pet& other) = delete;

    ~Pet() {}

    const string& GetName() const { return mName; }

private:
    string mName;

};

class Person final
{
public:
    Person() = default;
    Person(shared_ptr<Pet> pet)
        : mPet(pet)
    {}

    Person(const Person& other) = delete;

    ~Person() {}

    void PrintPetName() const { cout << "Pet's name: " << mPet->GetName() << endl; }

private:
    shared_ptr<Pet> mPet;

};

int main(void)
{
    shared_ptr<Pet> pPet = make_shared<Pet>("Chorong");
    cout << "pPet's RefCount: " << pPet.use_count() << endl;

    shared_ptr<Person> pPark = make_shared<Person>(pPet);
    pPark->PrintPetName();
    cout << "pPet's RefCount: " << pPet.use_count() << endl;
    cout << "pPark's RefCount: " << pPark.use_count() << endl;

    return 0;
}

 

    Ex07030404)

<hide/>

// main.cpp

#include <iostream>
#include <string>

using namespace std;

class Person;

class Pet final
{
public:
    Pet() = default;
    Pet(const string& name)
        : mName(name)
    {}

    Pet(const Pet& other) = delete;

    ~Pet() { cout << "[Pet] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetOwner(shared_ptr<Person> owner) { mOwner = owner; }

private:
    string mName;
    shared_ptr<Person> mOwner;

};

class Person final
{
public:
    Person() = default;
    Person(const string& name)
        : mName(name)
    {}

    Person(const Person& other) = delete;

    ~Person() { cout << "[Person] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetPet(shared_ptr<Pet> pet) { mPet = pet; }

private:
    string mName;
    shared_ptr<Pet> mPet;

};

int main(void)
{
    shared_ptr<Pet> pPet = make_shared<Pet>("Chorong");
    shared_ptr<Person> pPark = make_shared<Person>("Park");
    cout << "pPet's RefCount: " << pPet.use_count() << endl;
    cout << "pPark's RefCount: " << pPark.use_count() << endl;

    pPark->SetPet(pPet);
    pPet->SetOwner(pPark); // 순환 참조
    cout << "pPet's RefCount: " << pPet.use_count() << endl;
    cout << "pPark's RefCount: " << pPark.use_count() << endl;

    return 0; 
}

// 실행 결과, 소멸자가 호출되지 않음.

 

 

7.3 weak_ptr

  7.3-1 weak_ptr

    Note) weak_ptr은 순환 참조를 막을 수 있는 방법

      물론 순환 참조만을 막기 위해 나온 것은 아님.

      원래 목적의 weak_ptr은 원시 포인터를 전달 받은 후,

      쓸려고 할 때 지워져 있는 경우를 판단하려고 나온 것.

      그런데 원래 목적대로는 잘 안쓰이고 있음.

      앞에서 언급된 RAII 방식을 위배하게 되기 때문.

      포인터를 전달하는 거 자체가 잘못되기도 했지만,

      전달해서 쓰이는 도중에 지워진걸 안다는게 무슨의미일까.

      전달 해준 원래 개체에서는 그럼 RAII를 안지키고 넘어가도될까?

 

    Note) 약한 참조는 원시 포인터 해제에 영향을 끼치지 않음.

      약한 참조는 그저 약한 참조의 수를 결정하는데 사용됨.

      어찌되었든, 약한 참조로 참조되는 개체는 

      강한 참조 카운트가 0이 될 때 소멸됨.

      순환 참조 문제의 해결책

 

    Note) weak_ptr 만드는 방법

      공유 포인터에서부터 약한 포인터를 만듦.

template< class U >
weak_ptr& operator=(const shared_ptr<U>& other) noexcept;

 

    Ex07030101)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class Pet final
{
public:
    Pet() = default;
    Pet(const string& name)
        : mName(name)
    {}

    Pet(const Pet& other) = delete;

    ~Pet() { cout << "[Pet] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetOwner(shared_ptr<Person> owner) { mOwner = owner; }

private:
    string mName;
    shared_ptr<Person> mOwner;

};

int main(void)
{
    shared_ptr<Pet> pPet1 = make_shared<Pet>("Chorong");
    cout << "pPet1's RefCount: " << pPet1.use_count() << endl;
    weak_ptr<Pet> pPet2 = pPet1;
    cout << "pPet1's RefCount: " << pPet1.use_count() << endl;
    cout << "pPet2's RefCount: " << pPet2.use_count() << endl;

    return 0;
}

 

    Def) lock()와 expired()

      반환 값을 통해 해당 weak_ptr의 shared_ptr이 존재하는지 확인 가능.

 

    Ex07030102)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class Pet final
{
public:
    Pet() = default;
    Pet(const string& name)
        : mName(name)
    {}

    Pet(const Pet& other) = delete;

    ~Pet() { cout << "[Pet] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetOwner(shared_ptr<Person> owner) { mOwner = owner; }

private:
    string mName;
    shared_ptr<Person> mOwner;

};

int main(void)
{
    shared_ptr<Pet> pPet1 = make_shared<Pet>("Chorong");
    weak_ptr<Pet> pPet2 = pPet1;

    if (nullptr != pPet2.lock()) { cout << "pPet2 has a shared_ptr." << endl; }

    pPet1.reset();

    if (nullptr == pPet2.lock()) { cout << "pPet2 does not have a shared_ptr." << endl; }

    return 0;
}

 

    Ex07030103)

<hide/>

// main.cpp

#include <iostream>

using namespace std;

class Pet final
{
public:
    Pet() = default;
    Pet(const string& name)
        : mName(name)
    {}

    Pet(const Pet& other) = delete;

    ~Pet() { cout << "[Pet] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetOwner(shared_ptr<Person> owner) { mOwner = owner; }

private:
    string mName;
    shared_ptr<Person> mOwner;

};

int main(void)
{
    shared_ptr<Pet> pPet1 = make_shared<Pet>("Chorong");
    weak_ptr<Pet> pPet2 = pPet1;

    if (false == pPet2.expired()) { cout << "pPet2 has a shared_ptr." << endl; }

    pPet1 = nullptr;

    if (true == pPet2.expired()) { cout << "pPet2 does not have a shared_ptr." << endl; }

    return 0;
}

 

 

  7.3-2 weak_ptr 활용

    Note) weak_ptr의 활용

      Ex07030201은 순환 참조를 막기 위해서 weak_ptr을 씀.

      Ex07030202은 간단한 캐시를 weak_ptr을 이용해서 구현함.

      캐시 테이블에 있는 데이터가 끝까지 존재할 수도 있고,

      캐시 테이블에는 있다고 한 데이터가 실제로는 사라졌을 수도 있음.

      그럼 그 기반을 shared_ptr 기반으로 짤 순 없겠구나 싶기 때문.

 

    Ex07030201)

<hide/>

// main.cpp

#include <iostream>
#include <string>

using namespace std;

class Person;

class Pet final
{
public:
    Pet() = default;
    Pet(const string& name)
        : mName(name)
    {}

    Pet(const Pet& other) = delete;

    ~Pet() { cout << "[Pet] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetOwner(weak_ptr<Person> owner) { mOwner = owner; }

private:
    string mName;
    weak_ptr<Person> mOwner;

};

class Person final
{
public:
    Person() = default;
    Person(const string& name)
        : mName(name)
    {}

    Person(const Person& other) = delete;

    ~Person() { cout << "[Person] Destructor has been called." << endl; }

    const string& GetName() const { return mName; }
    void SetPet(shared_ptr<Pet> pet) { mPet = pet; }

private:
    string mName;
    shared_ptr<Pet> mPet;

};

int main(void)
{
    shared_ptr<Person> park = make_shared<Person>("Park");
    shared_ptr<Pet> chorong = make_shared<Pet>("Park");
    cout << "Owner: " << park.use_count() << ", " << "Pet: " << chorong.use_count() << endl;

    chorong->SetOwner(park);
    park->SetPet(chorong);
    cout << "Owner: " << park.use_count() << ", " << "Pet: " << chorong.use_count() << endl;

    return 0;
}

 

    Ex07030202)

<hide/>

// main.cpp

#include <iostream>
#include <map>
#include <string>
#include <assert.h>

using namespace std;

class MyVector final
{
public:
    MyVector(void) = default;
    MyVector(const float& x, const float& y)
        : mX(x)
        , mY(y)
    {}

    MyVector(const MyVector& other) = delete;

    ~MyVector(void) {}

    float GetX(void) const { return mX; }
    float GetY(void) const { return mY; }

private:
    float mX;
    float mY;
};

class SimpleCache final
{
public:
    SimpleCache() = default;

    SimpleCache(const SimpleCache& other) = delete;

    ~SimpleCache() {};

    void Add(const string& key, weak_ptr<MyVector> ptr)
    {
        auto it = mCache.find(key);
        if (mCache.end() == it)
        {
            mCache.insert(pair<string, weak_ptr<MyVector>>(key, ptr));
            return;
        }

        mCache[key] = ptr;
    }

    shared_ptr<MyVector> Get(string key)
    {
        auto it = mCache.find(key);
        if (mCache.end() == it) { return shared_ptr<MyVector>(); }
        if (true == it->second.expired())
        {
            mCache.erase(it);
            return shared_ptr<MyVector>();
        }

        return it->second.lock();
    }


private:
    map<string, weak_ptr<MyVector>> mCache;

};

int main(void)
{
    shared_ptr<MyVector> v1 = make_shared<MyVector>(0, 1);
    shared_ptr<MyVector> v2 = make_shared<MyVector>(2, 5);
    shared_ptr<MyVector> v3 = make_shared<MyVector>(8, 4);

    SimpleCache cache;
    cache.Add("Candy", v1);
    cache.Add("IceCream", v2);
    cache.Add("Chocolate", v3);
    shared_ptr<MyVector> cachedVector2 = cache.Get("IceCream");
    cout << "Usage of cachedVector2: " << cachedVector2.use_count() << endl;
    cout << "IceCream values: X: " << cachedVector2->GetX() << ", Y: " << cachedVector2->GetY() << endl;
    cache.Add("IceCream", v3);
    cachedVector2 = cache.Get("IceCream");
    cout << "IceCream values: X: " << cachedVector2->GetX() << ", Y: " << cachedVector2->GetY() << endl;
    v1 = nullptr;
    shared_ptr<MyVector> cachedVector1 = cache.Get("Candy");
    assert(cachedVector1 == nullptr);

    return 0;
}

 

    Note) shared_ptr Vs. weak_ptr

shared_ptr weak_ptr
강한 참조 약한 참조
강한 참조 카운드를 늘림 약한 참조 카운트를 늘림
직접적으로 사용할 수 있음. 직접적으로 사용할 수 없음.
원시 포인터가 확실히 존재하기 때문 lock()을 써서 shared_ptr가 여전히 존재하는지 확인해야 함.

'C++ > 문법 정리' 카테고리의 다른 글

Chapter 08. 이동 문법  (0) 2022.05.09
Chapter 06. C++11/14/17  (0) 2022.05.08
Chapter 05. 템플릿과 파일 시스템  (0) 2022.05.07
Chapter 04. 캐스팅과 인라인  (0) 2022.05.06
Chapter 03. 상속과 다형성  (0) 2022.05.05

댓글