본문 바로가기
C++ STL, 알고리즘

스마트 포인터 c++

by wanna_dev 2024. 10. 7.

 

스마트 포인터가 스코프를 벗어나게 되거나 리셋되면 거기에 할당된 리소스가 자동으로 해제된다.

스마트 포인터는 함수 스코프 안에서 동적으로 할당된 리소스를 관리하는데 사용할 수도있고,, 클래스의 데이터 멤버로 사용할 수도있다.

동적으로 할당된 리소스의 소유권을 함수의 인수로 넘겨줄 때도 스마트 포인터를 활용한다.

std::unique_ptr :고유 소유권 방식 지원

앨리어싱 : 어떤 포인터의 복사본을 여러 객체나 코드에서 갖고 있을때.

→ 모든 리소스를 제대로 해제하려면 리소스를 마지막으로 사용한 포인터가 해제해야한다. 그런데 코드의 어느 지점에서 그 리소스를 마지막으로 사용하는지 알기 힘들때가 많다.

→ 그래서 리소스의 소유자를 추적하도록 레퍼런스 카운팅을 구현한 스마트 포인터도 있다.

std::shared_ptr : 공유 소유권 방식 지원

두 포인터를 사용하려면 <memory> header를 인클루드 해야함

unique_ptr 생성방법

void leaky(){
	Simple* mySimplePtr = new Simple(); // 버그 : 메모리를 해제하지 않았다.
	mySimplePtr -> go();
}
void couldBeLeaky(){
	Simple* mySimplePtr = new Simple();
	mySimplePtr -> go(); // go()메서드에 익셉션이 발생하면 delete가 실행되지 않는다.
	delete mySimplePtr;
}
void notLeaky(){
	auto mySimpleSmartPtr = make_unique<Simple>();
	mySimpleSmartPtr -> go();
}

//만약 make_unique를 지원하지 않는 컴파일러를 사용한다면,
unique_ptr<Simple> mySimplePtr(new Simple());

foo(make_unique<Simple>(), make_unique<Bar>(data()))

unique_ptr 사용방법

auto mySimpleSmartPtr = make_unique<Simple>();
mySimpleSmartPtr -> go();
(*mySimpleSmartPtr).go();

void processData(Simple* simple){}

processData(mySimpleSmartPtr.get());

mySimpleSmartPtr.reset(); //리소스 해제 후 nullptr로 초기화
mySimpleSmartPtr.reset(new Simple()); //리소스 해제 후 새로운 Simple 인스턴스로 설정
Simple* simple = mySimpleSmartPtr.release(); //소유권 해제
delete simple;
simple = nullptr;

auto mySmartArr = make_unique<int[]>(10);

unique_ptr는 단독 소유권을 표현하기 때문에 복사할 수 없다.

std::move() 유틸리티를 사용하면 하나의 unique_ptr를 다른 곳으로 이동할 수 있다.

복사라기 보다는 이동의 개념이다.

class Foo
{
public:
	Foo(unique_ptr<int> data : mData(move(data)){}
private:
	unique_ptr<int> mData;
};

auto myIntSmartPtr = make_unique<int>(42);
Foo f(move(myIntSmartPtr));

int* malloc_int(int value){
	int* p = (int*)malloc(sizeof(int));
	*p = value;
	return p;
}

int main(){
	unique_ptr<int, decltype(free)*> myIntSmartPtr(malloc_int(42), free);
	return 0;
}

shared_ptr

auto mySimpleSmartPtr = make_shared<Simple>();
shared_ptr<int> myIntSmartPtr(malloc_int(42), free);
void CloseFile(FILE* filePtr){
	if(filePtr == nullptr)return;
	fclose(filePtr);
	cout<<"File closed."<<'\\n';
}

int main(){
	FILE* f = fopen("data.txt", "w");
	shared_ptr<FILE> filePtr(f, CloseFile);
	if(filePtr == nullptr){
		cerr << "Error opening file." << endl;
	}else{
		cout<<"File opened"<<endl;
	}
}

shared_ptr 캐스팅하기

shared_ptr 캐스팅하는 함수로

const_pointer_cast(), dynamic_pointer_cast(), static_pointer_cast()가 제공된다.

C++ 17부터 reinterpret_pointer_cast()

레퍼런스 카운팅이 필요한 이유

레퍼런스 카운팅은 어떤 클래스의 인스턴스 수나 현재 사용중인 특정한 객체를 추적하는 메커니즘이다.

레퍼런스 카운팅을 지원하는 스마트 포인터는 실제 포인터를 참조하는 스마트 포인터 수를 추적한다. 그래서 스마트 포인터가 중복 삭제되는 것을 방지한다.

중복 삭제 문제는 재현하기 쉽다.

void doubleDelete()
{
	Simple* mySimple = new Simple();
	shared_ptr<Simple> smartPtr1(mySimple);
	shared_ptr<Simple> smartPtr2(mySimple);
}

//result
//Simple constructor called!
//Simple destructor called!
//Simple destructor called!

생성자는 한번 호출되고 소멸자는 두번 호출되는 이상한 현상이 발생한다.

void noDoubleDelete(){
	auto smartPtr1 = make_shared<Simple>();
	shared_ptr<Simple> smartPtr2(smartPtr1);
}

//Simple constructor called
//Simple destructor called

다음처럼 복사본을 만들어 사용하면 된다.

shared_ptr는 앨리어싱을 지원한다.

그래서 한 포인터(소유한 포인터)를 다른 shared_ptr와 공유하면서 다른객체(저장된 포인터)를 가리킬 수 있다.

예를 들어 shared_ptr가 객체를 가리키는 동시에 그 객체의 멤버도 가리키게 할 수 있다.

class Foo{
public:
	Foo(int value) : mData(value){}
	int mData;
};

auto foo = make_shared<Foo>(42);
auto aliasing = shared_ptr<int>(foo, &foo->mData);

여기서 두 shared_ptr (foo와 aliasing)가 모두 삭제될 때만 Foo 객체가 삭제된다.

소유한 포인터는 레퍼런스 카운팅에 사용하는 반면, 저장된 포인터는 포인터를 역참조하거나 그 포인터에 대해 get()을 호출할 때 리턴된다.

owner_before() 메서드나 std::owner_less 클래스를 사용하여 소유한 포인터에 대해 비교연산을 수행해도 된다.

weak_ptr

shared_ptr가 가리키는 리소스의 레퍼런스를 관리하는데 사용된다.

weak_ptr는 리소스를 직접 소유하지 않기 때문에 shared_ptr가 해당 리소스를 해제하는 데 아무런 영향을 미치지 않는다.

weak_ptr는 삭제될 때(예를들어 스코프를 벗어날 때) 가리키던 리소스를 삭제하지 않고, shared_ptr가 그 리소스를 해제했는지 알아낼 수 있다.

weak_ptr의 생성자는 shared_ptr나 다른 weak_ptr를 인수로 받는다.

weak_ptr에 저장된 포인터에 접근하려면 shared_ptr로 변환해야한다.

  • 변환방법:
  1. weak_ptr 인스턴스의 lock() 메서드를 이용하여 shared_ptr를 리턴받는다. 이때 shared_ptr에 연결된 weak_ptr가 해제되면 shared_ptr의 값은 nullptr이 된다.
  2. shared_ptr의 생성자에 weak_ptr를 인수로 전달해서 shared_ptr를 새로 생성한다. 이때 shared_ptr에 연결된 weak_ptr가 해제되면 std::bad_weak_ptr Exception이 발생한다.
void useResource(weak_ptr<Simple>& weakSimple){
	auto resource = weakSimple.lock();
	if(resource){
		cout<<"Resource still alive" << endl;
	}
	else{
		cout<<"Resource has been freed" << endl;
	}
}

int main(){
	auto sharedSimple = make_shared<Simple>();
	weak_ptr<Simple> weakSimple(sharedSimple);
	
	//weak_ptr를 사용한다.
	useResource(weakSimple);
	
	//shared_ptr를 리셋한다.
	//Simple 리소스에 대한 shared_ptr는 하나뿐이므로 weak_ptr가 살아 있더라도 리소스가 해제된다.
	sharedSimple.reset();
	
	//weak_ptr를 한번 더 사용한다.
	useResource(weakSimple);

	return 0;
}

//실행결과
//Simple constructor called
//Resource still alive
//Simple destructor called
//Resource has been freed

이동 의미론

표준 스마트 포인터인 shared_ptr와 unique_ptr, weak_ptr는 모두 성능 향상을 위해 이동 의미론을 지원한다.

unique_ptr<Simple> create(){
	auto ptr = make_unique<Simple>();
	return ptr;
}

int main(){
	unique_ptr<Simple> mySmartPtr1 = create();
	auto mySmartPtr2 = create();
	return 0;
}

enable_shared_from_this

믹스인 클래스인 std::enable_shared_from_this를 이용하면 객체의 메서드에서 shared_ptr이나 weak_ptr를 안전하게 리턴할 수 있다.

shared_from_this() : 객체의 소유권을 공유하는 shared_ptr를 리턴한다.

weak_from_this() : 객체의 소유권을 추적하는 weak_ptr를 리턴한다.

class Foo : public enable_shared_from_this<Foo>{
public: 
	shared_ptr<Foo> getPointer(){
		return shared_from_this();	
	}
};

int main(){
	auto ptr1 = make_shared<Foo>();
	auto ptr2 = ptr1 -> getPointer();
}

객체의 포인터가 shared_ptr에 이미 저장된 상태에서만 객체에 shared_from_this()를 사용할 수 있다.

 

 

참조 : 전문가를 위한 c++

'C++ STL, 알고리즘' 카테고리의 다른 글

파생 클래스의 복제 생성자와 대입 연산자  (0) 2024.10.07
Virtual  (0) 2024.10.07
memory leak detection  (0) 2024.10.02
C++ 상속  (3) 2024.10.02
const  (0) 2024.10.02