[C++] 스마트 포인터 unique_ptr, shared_ptr, weak_ptr
C+11에 템플릿으로 구현된 세가지의 스마트포인터 unique_ptr, shared_ptr, weak_ptr가 추가되었다.
아마도 가장 많이 사용하고 있지 않은가 싶다.
특히나 C++로 게임을 만드려고 한다면 객체의 생명주기 관리를 위해 잘 알아두는 것이 좋겠다.
1. unique_ptr
대입 연산자를 막아버렸다.
의도는 명확하다.
다른곳에서 참조하지 말라는 의미다.
물론 get을 호출하면 raw pointer를 얻을 수 있다.
그럼 사실상 무의미 해진다.
#include <memory>
#include <iostream>
class MyInstance
{
public:
void operator delete(void* pAddress)
{
std::cout << "MyInstance Delete Operator" << std::endl;
}
void SayHello () { std::cout << "Hello" << std::endl; }
MyInstance () { std::cout << "Constructed." << std::endl; }
virtual ~MyInstance () { std::cout << "Destroied " << std::endl; }
};
int main()
{
if (1)
{
std::unique_ptr<MyInstance> A = std::unique_ptr<MyInstance>(new MyInstance);
A->SayHello();
}
//unique_ptr가 소멸될때 MyInstance 객체의 메모리에 대해 delete가 호출된다.
//std::unique_ptr<MyInstance> B = A;
//unique_ptr 은 대입연산자를 사용할수 없다.
//MyInstance* RawPointer = A.get();
//std::unique_ptr<MyInstance> B = std::unique_ptr<MyInstance>(A.get());
//get은 Raw pointer 를 반환한다.
//가급적 사용하지 말것.
//unique_ptr 구현 의도와 어긋남.
}
블럭을 벗어나니 지역변수인 A는 해제된다.
해제될때 MyInstance 객체 주소에 대해서도 delete가 호출 되었다.
뭐 이런짓을 하는 사람은 없겠지만...
당연히 지역변수의 주소를 넘기는 일은 하지 않도록 한다.
SomeClass MyIns;
if (1)
{
std::unique_ptr<SomeClass> A = std::unique_ptr<SomeClass>(&MyIns);
}
//delete 가 따로 정의 되어 있지 않거나,
//재정의된 delete가 free를 호출 하는 경우.
//unique_ptr<SomeClass>
//스택메모리 내부의 주소에 대해 free가 호출 될 것이다.
//크래시.
2. shared_ptr
레퍼런스 카운트를 사용해서 사용 / 미사용을 체크하도록 하도록 구현되어 있다.
레퍼런스 카운트가 0이 되면 해당 주소에 대해 delete 를 호출하도록 되어있다.
#include <memory>
#include <iostream>
class MyInstance
{
public:
void operator delete(void* pAddress)
{
std::cout << "MyInstance Delete Operator" << std::endl;
}
void SayHello () { std::cout << "Hello" << std::endl; }
MyInstance () { std::cout << "Constructed." << std::endl; }
virtual ~MyInstance () { std::cout << "Destroied " << std::endl; }
};
void Function1(std::shared_ptr<MyInstance>& pIns)
{
std::cout << pIns.use_count() << std::endl;
//pIns 참조변수 사용시 pIns는 새로운 shared_ptr객체가 아니므로
//레퍼런스 카운트는 그대로이다.
//사실상 pIns->get()을 넘긴것과 같다.
}
void Function2(std::shared_ptr<MyInstance> pIns)
{
std::cout << pIns.use_count() << std::endl;
//내부에서 레퍼런스 카운트가 한번 증가되고,
//스택이 정리되며 객체가 소멸하고 레퍼런스 카운트가 다시 감소한다.
}
int main()
{
std::shared_ptr<MyInstance> Instance = std::shared_ptr<MyInstance>(new MyInstance);
std::cout << Instance.use_count() << std::endl;
{
std::shared_ptr<MyInstance> Ref = std::shared_ptr<MyInstance>(Instance);
std::cout << Instance.use_count() <<std::endl;
//레퍼런스 카운트가 1 증가한다.
Function2(Instance);
//함수 내부에서 한번 증가되고, 스택이 정리되며 감소된다
}
std::cout << Instance.use_count() << std::endl;
//지역변수가 소멸될때 레퍼런스 카운트가 감소 되었다.
}
reset 멤버함수가 있다.
포인팅 하고 있던 객체를 nullptr로 초기화한다.
당연히 이 함수를 호출하면 해당 객체에 대한 레퍼런스 카운트는 다시 감소한다.
std::shared_ptr<MyInstance> Instance = std::shared_ptr<MyInstance>(new MyInstance);
Instance.reset();
//reset을 호출하면 포인팅하고 있던 객체에 대한 레퍼런스 카운트가 1 감소한다.
순환참조 / 상호참조 문제가 있다.
이 점은 유의해야 한다.
아래를 보도록 하자.
#include <memory>
#include <iostream>
#include <vector>
class Container;
class Element
{
public:
std::shared_ptr<Container> WhereAmI;
void operator delete(void* pAddress)
{
std::cout << "Element Delete Operator" << std::endl;
}
};
class Container
{
public:
std::vector<std::shared_ptr<Element>> Elements;
void operator delete(void* pAddress)
{
std::cout << "Cotainer Delete Operator" << std::endl;
}
};
int main()
{
if (1)
{
std::shared_ptr<Container> ContainerIns = std::shared_ptr<Container>(new Container);
for (int i = 0; i < 10; ++i)
{
Element* p = new Element;
p->WhereAmI = ContainerIns;
ContainerIns->Elements.push_back(std::shared_ptr<Element>(p));
}
}
//shared_ptr 객체인 CotainerIns는 소멸된다.
//10개의 Element가 Cotainer 객체를 참조중이므로 실제 객체는 소멸하지 않는다.
//Container 객체가 소멸할때 vector가 clear되고, Element들의 레퍼런트 카운트가 0이되고, 소멸할것이다.
//이때 어떻게 할 것인가?
}
이럴 경우 수작업을 거쳐야 한다.
if (1)
{
std::shared_ptr<Container> ContainerIns = std::shared_ptr<Container>(new Container);
for (int i = 0; i < 10; ++i)
{
Element* p = new Element;
p->WhereAmI = ContainerIns;
ContainerIns->Elements.push_back(std::shared_ptr<Element>(p));
}
ContainerIns->Elements.clear();
//이러면 해결되긴 한다만 살짝 부자연 스럽다.
//매번 이래야 한다면 실수의 여지가 언젠가는 생긴다.
//사실상 수작업이다.
}
vector 컨테이너안의 Element 객체에 대한 shared_ptr들이 소멸하며, Element 레퍼런트 카운트가 0이되고,
하나씩 Element 객체는 소멸할때, 각 Element 객체는 자신이 멤버로 가지고 있는 컨테이너에 대한 shared_ptr에 대한 소멸자 또한 호출한다.
그리고 결국 컨테이너 또한 종래엔 해제된다.
해제가 되었으니 된 것인가?
사실상 이러면 shared_ptr 을 사용하는 의미가 무의미 해진다.
내가 실수하거나 놓치는 부분을 객체의 기능이 담당하게 하도록 하는것이 스마트포인터의 사용 이유이기 때문이다.
이러한 순환참조 / 상호참조에 대한 문제로 weak_ptr이 등장한다.
3. weak_ptr
weak_ptr는 shared_ptr을 참조할 경우, shared_ptr의 레퍼런스 카운트에 영향을 주지 않는다.
위의 순환참조 / 상호참조 문제를 해결하기 위해 weak_ptr를 사용해보자.
#include <memory>
#include <iostream>
#include <vector>
class Container;
class Element
{
public:
std::weak_ptr<Container> WhereAmI;
void operator delete(void* pAddress)
{
std::cout << "Element Delete Operator" << std::endl;
}
};
class Container
{
public:
std::vector<std::shared_ptr<Element>> Elements;
void operator delete(void* pAddress)
{
std::cout << "Cotainer Delete Operator" << std::endl;
}
};
int main()
{
if (1)
{
std::shared_ptr<Container> ContainerIns = std::shared_ptr<Container>(new Container);
for (int i = 0; i < 10; ++i)
{
Element* p = new Element;
p->WhereAmI = ContainerIns;
//Element가 Cotainer를 참조할때는 weak ptr로 참조한다.
ContainerIns->Elements.push_back(std::shared_ptr<Element>(p));
}
}
//weak_ptr로 참조하면, shared_ptr의 레퍼런스 카운트에 영향을 미치지 않는다.
}
shared_ptr 은 weak_ptr이 참조를 하건 말건 알아서 객체를 해제한다.
weak_ptr는 단순히 참조를 위해서 태어났다.
해제된 메모리에 대한 확인이 가능하다.
어떻게 확인을 할까? 내부적으로 shared_ptr는 내부에 컨트롤 블럭을 두기 때문이다.
생각해보면 매우 당연한 이야기다.
이전 예제에서도 지역변수가 해제되어도 레퍼런트 카운트는 살아있었다.
동적으로 힙에 컨트롤 블럭이 할당되었고, 해당 컨트롤 블럭의 주소가
= 연산자로 스마트포인터 객체간 복사되고, 카운트가 증가 되었던 것이다.
아래를 보도록하자.
#include <memory>
#include <iostream>
#include <vector>
class MyInstance
{
public:
void operator delete(void* pAddress)
{
std::cout << "MyInstance Delete Operator" << std::endl;
}
void SayHello () { std::cout << "Hello" << std::endl; }
MyInstance () { std::cout << "Constructed." << std::endl; }
virtual ~MyInstance () { std::cout << "Destroied " << std::endl; }
};
int main()
{
//std::weak_ptr<MyInstance> Weak = std::weak_ptr<MyInstance>(new MyInstance);
//불가능.
//다음과 같이 사용한다.
std::weak_ptr<MyInstance> weak;
if (1)
{
std::shared_ptr<MyInstance> shared = std::shared_ptr<MyInstance>(new MyInstance);
weak = shared;
if (false == weak.expired())
std::cout << "Instance is alive" << std::endl;
else
std::cout << "Instance is expired" << std::endl;
}
//shared_ptr 객체는 소멸된 상태.
//실 객체또한 소멸된 상태.
//그렇다면 weak_ptr을 어떻게 써먹을 수 있을까?
//-> shared_ptr의 레퍼런스 카운트가 0이되고,
//포인팅하고 있는 객체가 소멸한다.
//shared_ptr 구현 안에는 참조 카운트를 위한 컨트롤 블럭이 들어가있다.
//포인팅 하고있는 객체의 소멸과 별개로,
//weak_ptr의 참조가 끝나기 전까지는
//확인하기 위한 shared_ptr의 컨트롤 블럭이 살아있다.
//즉, 컨트롤 블럭을 들여다보고 이를 확인하는 것이 가능하다는 이야기다.
//이는 expired로 확인 가능하다.
if (false == weak.expired())
std::cout << "Instance is alive" << std::endl;
else
std::cout << "Instance is expired" << std::endl;
}
위처럼 expired 를 통해 죽었는지 살았는지 확인이 가능하다.
weak_ptr 이 reset을 호출하고, 이미 실 객체가 해제되었고(shared_ptr refCount 0)
더 이상 컨트롤 블럭이 사용될 여지가 없을시
컨트롤 블럭 또한 해제될 것이다.
4. 스마트 포인터는 스레드 세이프 한가?
어떤 연산은 스레드 세이프하고, 어떤 연산은 스레드 세이프 하지 않다.
기본적으로, 컨트롤 블럭에 대한 연산은 스레드 세이프하다.
아래의 예시를 보도록 하자.
#include <iostream>
#include <memory>
#include <thread>
int main()
{
std::shared_ptr<int> S = std::make_shared<int>(10);
auto Function = [&S]()
{
for (int i = 0; i < 100000; ++i)
std::shared_ptr<int> temp = S;
//Okay.
//컨트롤 블럭이 temp에 복사된다.
//컨트롤 블럭에 대한 연산은 thread safe.
};
std::thread t1(Function);
std::thread t2(Function);
t1.join();
t2.join();
std::cout << "Reference Count After 2 Thread Terminated : " << S.use_count() << std::endl;
}
무리없이 원하는 결과가 나온다.
다음의 경우를 보자.
#include <iostream>
#include <memory>
#include <thread>
#include <crtdbg.h>
int main()
{
std::shared_ptr<int> S = std::make_shared<int>(10);
auto Function = [&S]()
{
for (int i = 0; i < 100000; ++i)
S = std::make_shared<int>(i);
//operator=은 안전하지 않다.
//참조하고 있는 컨트롤 블럭내의 연산은 안전하지만,
//어떤 컨트롤블럭 참조할지에 대한 레이스가 발생한다.
//->메모리 누수 발생.
};
std::thread t1(Function);
std::thread t2(Function);
t1.join();
t2.join();
_CrtDumpMemoryLeaks();
std::cout << "Reference Count After 2 Thread Terminated : " << S.use_count() << std::endl;
}
스마트포인터 객체 자체에 대한 대입연산시 데이터 레이스 발생으로, 메모리 누수가 발생한다.
std를 이용한 해결방법은 두가지가 있다.
첫째는 std의 스마트포인터를 위해 오버로딩된 atomic_operation을 사용하는 방법.
std::shared_ptr<int> S = std::make_shared<int>(10);
auto Function = [&S]()
{
for (int i = 0; i < 1000000; ++i)
std::atomic_store(&S, std::make_shared<int>(i));
//S = std::make_shared<int>(i);
//atomic_store로 교체.
};
이건 추천하지 않는 방법이며,
지금은 거의 사용하지 않는다.
아니다 굳이 사용할 이유가 없다.
왜 ? 큰 단점이 하나 있다.
글로벌 락을 잡아버린다.
이러면 스레드 동기화가 필요한 코드가 많다면 실행시마다 병목이 너무 심해진다.
그래서 C++20 이전에는 간단한 래핑 클래스를 만들어서
그 안에 공유자원을 넣어두고 동기화를 해서 스마트 포인터를 사용했는데,
C++20부터 std::atomic에 스마트 포인터 자료형을 사용할 수 있게 되었다.
#include <iostream>
#include <memory>
#include <thread>
#include <crtdbg.h>
int main()
{
std::atomic<std::shared_ptr<int>> S = std::make_shared<int>(10);
auto Function = [&S]()
{
for (int i = 0; i < 1000000; ++i)
S = std::make_shared<int>(10);
//operator =이 atomic하게 동작한다.
};
std::thread t1(Function);
std::thread t2(Function);
t1.join();
t2.join();
_CrtDumpMemoryLeaks();
std::cout << "Reference Count After 2 Thread Terminated : " << S.load().use_count() << std::endl;
}
'C++' 카테고리의 다른 글
[C++] const lvalue reference, rvalue reference (2) | 2022.03.23 |
---|---|
[C++] C++11 constexpr (0) | 2022.03.20 |
[C++] Type Casting 연산자 정리 static, dynamic,reinterpret, const (0) | 2022.03.17 |
[C++] C++ 11 Argument로 전달된 정적 배열의 크기 (0) | 2022.03.05 |
[C++] C++11 Parameter Pack 사용하기 (0) | 2022.03.05 |
댓글
이 글 공유하기
다른 글
-
[C++] const lvalue reference, rvalue reference
[C++] const lvalue reference, rvalue reference
2022.03.23 -
[C++] C++11 constexpr
[C++] C++11 constexpr
2022.03.20 -
[C++] Type Casting 연산자 정리 static, dynamic,reinterpret, const
[C++] Type Casting 연산자 정리 static, dynamic,reinterpret, const
2022.03.17 -
[C++] C++ 11 Argument로 전달된 정적 배열의 크기
[C++] C++ 11 Argument로 전달된 정적 배열의 크기
2022.03.05