shared pointer
[C++] Weak Pointer (위크 포인터) : Circular Reference (순환 참조) 해결
Shared Pointer를 이용하여 Circular Reference를 하여 Memory Leak이 발생할 수 있다고 공부를 했다. Weak Pointer의 개념에 대해서 공부하고 이를 이용하여 Circular Reference문제를 해결해 보자.
Weak Pointer (위크 포인터)
Weak Pointer는 말 그대도 약간 포인터이다. Shared Pointer를 참조하는 용도로 사용을 하게 되고 Weak Pointer가 Shared Pointer를 참조를 하게 되면 Weak Count가 증가하게 된다. Weak Pointer를 이용하여 오브젝트의 리소스를 접근할 수 없다. Weak Pointer를 사용을 하려면 반드시 Shared Pointer형을 반환해 주는 lock() 메서드를 이용하여 Shared Pointer로 변환을 해야 되는데 이때 참조하는 Shared Pointer의 Strong Count가 증가하게 된다는 것을 잊으면 안 된다.
아래는 Weak Pointer가 선언되고 Shared Pointer를 참조하는 코드이다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
virtual void speak() const
{
std::cout << "Hi?" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
std::weak_ptr<Cat> weak_kitty = kitty;
//weak pointer 선언 및 shared pointer kitty참조
std::cout << "count : " << kitty.use_count() << std::endl;
return 0;
}
위 코드를 보면 Shared Pointer kitty를 선언하고 Cat 오브젝트를 생성하였다. 그다음 Weak Pointer를 선언하여 kitty가 가리키고 있는 오브젝트를 참조를 하였다. 그다음 kitty가 가리키고 있는 오브젝트의 count를 출력한 결과 1이 출력이 되었다.
만약 Weak Pointer가 참조하고 있는 오브젝트의 리소스를 사용하고 싶다면 lock() 메서드를 통해 Shared Pointer를 반환받을 수 있다. 아래 코드는 Weak Pointer의 lock() 메서드를 통해 새로운 Shared Pointer를 생성하여 리소스를 사용하는 코드이다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
virtual void speak() const
{
std::cout << "Hi?" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
std::weak_ptr<Cat> weak_kitty = kitty;
if(const auto shared_kitty = weak_kitty.lock())
{
shared_kitty->speak();
}
else
{
std::cout << "pointing nothing\n";
}
return 0;
}
위 코드는 if문 조건문에 const auto shared_kitty에 Weak Pointer weak_kitty의 lock() 메서드를 통하여 Shared Pointer를 반환해주었다. 만약 가리키고 있는 Shared Pointer가 메모리 해제가 된 경우 empty shared_ptr를 반환해준다. 위 경우 weak_kitty가 가리키고 있는 kitty 오브젝트는 마메로 해제가 안되었기 때문에 해당 shared_pointer를 반환한다. if 조건문을 scope로 가지는 shared_kitty가 kitty 오브젝트를 가리키고 있기 때문에 kitty의 count가 2로 증가하게 되고 조건문이 끝나고 다시 1로 감소된다.
그렇다면 아래 코드를 보자.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
virtual void speak() const
{
std::cout << "Hi?" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::weak_ptr<Cat> weak_kitty;
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
weak_kitty = kitty;
}
if(const auto shared_kitty = weak_kitty.lock())
{
shared_kitty->speak();
}
else
{
std::cout << "pointing nothing\n";
}
return 0;
}
위 코드는 Weak Pointer는 main함수 scope에 선언이 되었고 Shared Pointer인 kitty는 Curly brace scope에 선언이 되었다. 먼저 괄호 안에 선언된 kitty에 Cat 오브젝트를 생성하고 main함수 scope에 있는 weak_kitty를 kitty오브젝트를 참조하게 했다. 괄호를 벗어나고 if문에서 weak_kitty의 lock() 메서드로 Shared Pointer를 반환하려고 했지만 kitty 오브젝트는 이미 괄호 안에서 할당 해제가 되었기 때문에 empty Share Pointer가 반환되어 pointing nothing을 출력하게 된다.
Circular Reference (순환 참조) 해결
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");
kitty->mVar = nabi;
nabi->mVar = kitty;
std::cout << "kitty count : " << kitty.use_count() << std::endl;
std::cout << "nabi count : " << nabi.use_count() << std::endl;
}
저번 포스팅에서 다루었던 오브젝트 내부의 Shared Pointer가 서로 다른 오브젝트를 가리키게 되어 count가 줄지 않아 메모리 해제가 안되었던 문제를 Weak Pointer로 해결할 수 있다. 단순하게 클래스 내부의 Shared Pointer를 Weak Pointer로 교체를 하면 문제는 해결된다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::weak_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");
kitty->mVar = nabi;
nabi->mVar = kitty;
std::cout << "kitty count : " << kitty.use_count() << std::endl;
std::cout << "nabi count : " << nabi.use_count() << std::endl;
}
이렇게 코드를 바꾸게 되면 count는 증가하지 않고 main함수 스코프를 가진 kitty와 nabi가 할당 해제가 되면서 각 오브젝트로 메모리 해제가 된다.
Ref.
'Modern C++' 카테고리의 다른 글
[C++] Template Type Deduction (템플릿 타입 추론) : Perfect Forwarding (0) | 2021.08.26 |
---|---|
[C++] Template Intro. (템플릿) : Function Template (함수 템플릿) (0) | 2021.08.26 |
[C++] Shared Pointer (쉐어드 포인터) : Circular Reference (순환 참조) (0) | 2021.08.25 |
[C++] Smart Pointer (스마트 포인터): Unique Pointer (유니크 포인터) (0) | 2021.08.23 |
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
[C++] Shared Pointer (쉐어드 포인터) : Circular Reference (순환 참조)
Unique Pointer에 이어서 Smart Pointer의 한 종류인 Shared Pointer에 대해서 정리한다.
Shared Pointer (쉐어드 포인터)
이름에서도 알 수 있듯이 해당 포인터는 Exclusive Ownership을 가지는 Unique pointer와 다르게 Shared Ownership을 가지게 된다. 그 말은 하나의 오브젝트를 여러 개의 포인터가 가질 수 있다. 하지만 모든 스마트 포인터는 RAII콘셉트를 제공을 해야 한다. Shared Pointer는 여러 개의 포인터가 가리킴에도 불구하고 어떻게 오브젝트의 메모리 할당을 해제시키는지 알아보자.
아래 코드는 쉐어드 포인터를 선언하는 코드이다.
class Dog
{
public:
Dog() : mAge { 0 }
{
std::cout << "dog Constructor" << std::endl;
}
~Dog()
{
std::cout << "Dog destructor" << std::endl;
}
private:
int mAge;
}
int main()
{
std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
}
위 코드에서 dogPtr이라는 쉐어드 포인터를 생성하고 Dog객체를 할당하였다. 위 과정에서 해당 포인터의 scope단위는 main함수이고 객체가 생성이 되면서 Constructor를 부르게 되고 main함수가 종료되면서 자동적으로 Destrucotr를 호출하게 된다.
하지만 쉐어드 포인터의 특성으로 다른 쉐어드 포인터가 하나의 오브젝트를 가리킬 수 있다. 아래의 코드는 여러 개의 쉐어드 포인터가 하나의 오브젝트를 가리키는 예이다.
int main()
{
std::shared_ptr<Dog> dogPtr = std::make_shared<Dog>();
std::shared_ptr<Dog> dogPtr1 = dogPtr;
}
위와 같은 경우 Unique Pointer와 다르게 하나의 오브젝트를 두 개의 포인터가 가리키게 된다. 만약에 여기서 끝이라면 일반 포인터와 다를 바가 없다. 일반 포인터와 다른 점은 가리키는 메모리 공간에 몇 개의 포인터가 오브젝트를 가리키는지 Count를 한다. Count 중에서도 Strong Count와 Weak Count가 있는데 쉐어드 포인터가 가리키게 되면 Strong Count가 올라가고 오브젝트는 Strong Count가 0일 때 메모리를 해제를 결정하게 된다. Weak Pointer가 가리키게 되는 경우는 Weak Count가 올라가지만 메모리를 해제하기 위해서 참고되지는 않는다.
Shared Pointer은 개발자가 Rsource의 Life Cycle을 고려하지 않고(메모리 해제를 고려하지 않고) 한 오브젝트의 Ownership을 여러 Scope에서 공유가 가능하게 만들어준다.
하지만 이러한 특성 때문에 의도치 않은 Memory Leak이 일어날 수 있다.
아래 코드는 Shared Pointer 사용함에 있어서 Memory Leak이 일어나는 가장 쉬운 예이다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
kitty->mVar = kitty;
std::cout << kitty.use_count() << std::endl;
}
먼저 클래스 내에 Shared Pointer mVar를 추가로 생성하였다. main함수에서 Shared Pointer kitty를 선언하고 오브젝트를 생성하고 오브젝트 내의 Shared Pointer mVar를 오브젝트 자신인 kitty를 가리키게 만들었다. 그다음 kitty 오브젝트의 Strong Count를 확인하기 위해 use_count()를 이용해 출력을 한 결과 2가 출력이 된다. 이 경우 Memory Leak이 발생하게 되는데 아래 그림을 통해 알아보자.
Shared Pointer인 kitty는 main함수 scope가 종료됨에 따라 메모리 해제가 된다. kitty가 사라지고 오브젝트의 Count가 2에서 1로 줄게 된다. Shared Pointer가 해제되고 오브젝트 또한 자동으로 해제되는 것을 기대했지만 오브젝트 내부의 Shared Pointer가 오브젝트를 아직 가리키고 있기 때문에 main함수가 종료됐음에도 불구하고 메모리 해제가 안되어 Memory Leak이 일어난 경우이다.
위 예는 Memory Leak이 일어날 수 있다는 것을 보여주기 위한 예로 실제 많이 발생되지 않는다. 실제로 자주 발생되는 Memory Leak에 대한 코드는 아래와 같다.
Circular Reference (순환 참조)
#include <iostream>
#include <memory>
class Cat
{
public:
Cat(std::string name) : mName{ name }
{
std::cout << mName << " cat constructor" << std::endl;
}
~Cat()
{
std::cout << mName << " cat destructor" << std::endl;
}
std::shared_ptr<Cat> mVar;
private:
std::string mName;
};
int main()
{
std::shared_ptr<Cat> kitty = std::make_shared<Cat>("kitty");
std::shared_ptr<Cat> nabi = std::make_shared<Cat>("nabi");
kitty->mVar = nabi;
nabi->mVar = kitty;
std::cout << "kitty count : " << kitty.use_count() << std::endl;
std::cout << "nabi count : " << nabi.use_count() << std::endl;
}
순환 참조란 위 코드와 그림처럼 클래스 내부에서 Shared Pointer로 다른 클래스가 서로 가리키는 것을 의미한다. kitty 오브젝트와 nabi 오브젝트가 Shared Pointer kitty와 nabi에 의해서 가리키게 되고 내부의 mVar Shared Pointer가 서로의 오브젝트를 가리키게 되면 Shared Pointer가 main함수에서 메모리 해제가 되어도 각 오브젝트의 Count는 1로 메모리 해제가 안 되는 것을 말한다.
위와 같은 Memory Leak을 해결하기 위해 Weak_ptr을 이용하여 Circular Reference를 구현할 수 있다. 다음 포스팅은 Weak_Ptr에 대해서 공부할 예정이다.
Ref.
https://www.youtube.com/watch?v=tg34hwP0P0M&list=PLDV-cCQnUlIbOBiPvBaRPezOLArubgZbQ&index=4
'Modern C++' 카테고리의 다른 글
[C++] Template Intro. (템플릿) : Function Template (함수 템플릿) (0) | 2021.08.26 |
---|---|
[C++] Weak Pointer (위크 포인터) : Circular Reference (순환 참조) 해결 (0) | 2021.08.25 |
[C++] Smart Pointer (스마트 포인터): Unique Pointer (유니크 포인터) (0) | 2021.08.23 |
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
[C++] Object Slicing (오브젝트 슬라이싱) (0) | 2021.08.22 |