M.1 Intro to smart pointers and move semantics

주홍영·2022년 3월 22일
0

Learncpp.com

목록 보기
191/199

https://www.learncpp.com/cpp-tutorial/intro-to-smart-pointers-move-semantics/

memory allocation이 논리적 오류로 인해 해제되지 않을 수 있다
이러한 문제점을 해결하려면 어떻게 해야할까?

Smart pointer classes to the rescue?

클래스의 가장 큰 장점 중 하나는 destructor를 내장하고 있다는 것이다
destructor는 object가 scope를 벗어나서 소멸하면 자동으로 실행된다
따라서 우리가 constructor에서 memory를 할당했다면
destructor에서 memory를 해제할 수 있다

그렇다면 우리는 pointer를 관리하고 clean up하는데에 class를 사용할 수 있을까?
당연히 가능하다

다음의 예시를 보자

#include <iostream>

template <typename T>
class Auto_ptr1
{
	T* m_ptr;
public:
	// Pass in a pointer to "own" via the constructor
	Auto_ptr1(T* ptr=nullptr)
		:m_ptr(ptr)
	{
	}

	// The destructor will make sure it gets deallocated
	~Auto_ptr1()
	{
		delete m_ptr;
	}

	// Overload dereference and operator-> so we can use Auto_ptr1 like m_ptr.
	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
};

// A sample class to prove the above works
class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
	Auto_ptr1<Resource> res(new Resource()); // Note the allocation of memory here

        // ... but no explicit delete needed

	// Also note that the Resource in angled braces doesn't need a * symbol, since that's supplied by the template

	return 0;
} // res goes out of scope here, and destroys the allocated Resource for us

Auto_ptr1은 생성과 동시에 pointer를 받고 소멸될 때 destructor로 pointer를 해제한다
그리고 Resource 클래스에서 생성과 소멸 때 메세지가 출력되도록 하고 있다
위 프로그램의 출력은 다음과 같다

Resource acquired
Resource destroyed

Auto_ptr1 타입의 res는 local variable이므로 main scope를 벗어나면 소멸된다
따라서 Auto_prt1의 destructor가 항상 실행되게 되고 따라서 dynamically 할당된 메모리가 해제되는 것을 보장할 수 있게 된다

이러한 class를 smart pointer라고 한다
smart pointer는 동적으로 할당된 메모리를 관리하고 smart pointer 개체가 범위를 벗어날 때 메모리가 삭제되도록 설계된 구성 클래스입니다.(이와 관련하여 내장 포인터는 스스로 정리할 수 없기 때문에 "dumb pointer"라고도 합니다.)

A critical flaw

Auto_ptr1 클래스에는 일부 auto-generated 코드 뒤에 숨어 있는 치명적인 결함이 있다.
다음의 예시를 보자

#include <iostream>

// Same as above
template <typename T>
class Auto_ptr1
{
	T* m_ptr;
public:
	Auto_ptr1(T* ptr=nullptr)
		:m_ptr(ptr)
	{
	}

	~Auto_ptr1()
	{
		delete m_ptr;
	}

	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
};

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
	Auto_ptr1<Resource> res1(new Resource());
	Auto_ptr1<Resource> res2(res1); // Alternatively, don't initialize res2 and then assign res2 = res1;

	return 0;
}

해당 프로그램의 출력은 다음과 같다

Resource acquired
Resource destroyed
Resource destroyed

우리는 Auto_ptr1에 copy constructor를 정의해주지 않았다. 따라서 c++이 default로 제공하는 코드를 사용하게 될 것이다.따라서 우리가 res2를 res1으로 initialize 하게 되면 res1, 2모두 내장 포인터로 똑같은 값을 갖게 될 것이다
따라서 res1, 2가 소멸되는 시점에 똑같은 pointer를 두번이나 해제하게 되므로 crash가 발생한다

우리는 다음과 같은 상황에서도 비슷한 문제를 마주한다

void passByValue(Auto_ptr1<Resource> res)
{
}

int main()
{
	Auto_ptr1<Resource> res1(new Resource());
	passByValue(res1)

	return 0;
}

passByValue로 argument를 copy로 전달하기 때문에 함수 내부에서 res는 scope를 벗어나면 소멸된다. 이때 해제가 발생하게되고 따라서 main 함수가 끝날 때 crash가 발생하게 된다

우리는 그렇다면 이 문제를 어떻게 다뤄야 하나

우리가 할 수 있는 한 가지는 copy constructor와 assignment operator를 명시적으로 정의하고 delete하여 처음부터 copy가 만들어지는 것을 방지하는 것입니다. 그렇게 하면 pass by value case를 방지할 수 있습니다.

하지만 Auto_ptr1을 함수에서 어떻게 return 시킬 수 있을까?

??? generateResource()
{
     Resource* r{ new Resource() };
     return Auto_ptr1(r);
}

우리는 Auto_prt1을 reference로 return할 수 없다. 왜냐하면 function의 끝에서 Auto_ptr1은 destory되기 때문이다.

다른 방법으로는 copy constructor와 assignment operator를 override하여 deep copies를 하도록 하는 것이다. 이 방법으로는 우리는 적어도 same object에 pointer를 duplicate하는 경우는 방지할 수 있다. 그러나 copy는 expensive할 수 있고 우리는 불필요한 object를 만들고 싶지 않다. 더불어 dumb pointer를 assign하거나 initialize하는 것은 object를 복사하는 것도 아니다.

어떻게 해야 할까?

Move semantics

만약, copy constructor와 assignment operator로 pointer를 copy하는 것이 아닌,
ownership을 transfer/move 하면 어떠할까? 이것이 move semantics의 기저에 깔린 핵심 아이디어 이다
Move semantics(의미론)은 copy를 만들지 않고 ownership을 전달하는 것이다

다음의 예시를 보자

#include <iostream>

template <typename T>
class Auto_ptr2
{
	T* m_ptr;
public:
	Auto_ptr2(T* ptr=nullptr)
		:m_ptr(ptr)
	{
	}

	~Auto_ptr2()
	{
		delete m_ptr;
	}

	// A copy constructor that implements move semantics
	Auto_ptr2(Auto_ptr2& a) // note: not const
	{
		m_ptr = a.m_ptr; // transfer our dumb pointer from the source to our local object
		a.m_ptr = nullptr; // make sure the source no longer owns the pointer
	}

	// An assignment operator that implements move semantics
	Auto_ptr2& operator=(Auto_ptr2& a) // note: not const
	{
		if (&a == this)
			return *this;

		delete m_ptr; // make sure we deallocate any pointer the destination is already holding first
		m_ptr = a.m_ptr; // then transfer our dumb pointer from the source to the local object
		a.m_ptr = nullptr; // make sure the source no longer owns the pointer
		return *this;
	}

	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
	bool isNull() const { return m_ptr == nullptr;  }
};

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
	Auto_ptr2<Resource> res1(new Resource());
	Auto_ptr2<Resource> res2; // Start as nullptr

	std::cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
	std::cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");

	res2 = res1; // res2 assumes ownership, res1 is set to null

	std::cout << "Ownership transferred\n";

	std::cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
	std::cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");

	return 0;
}

위의 class Auto_ptr2를 보면
copy constructor의 경우 전달 받은 object의 내부 pointer를 nullptr로 할당하고 있따
또한 assignment operator의 경우에도 먼저 들고 있던 pointer의 메모리를 해제해주고
pointer를 복사한뒤에 source pointer를 nullptr로 할당하고 있다

따라서 위 프로그램의 출력은 다음과 같다

Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed

std::auto_ptr, and why it was a bad idea

지금이 std::auto_ptr에 대해 이야기하기에 적절한 시기입니다. C++98에서 도입되고 C++17에서 제거된 std::auto_ptr은 표준화된 스마트 포인터에 대한 C++의 첫 번째 시도였습니다. std::auto_ptr은 Auto_ptr2 클래스와 마찬가지로 이동 의미 체계를 구현하기로 결정했습니다.

그러나 std::auto_ptr(및 Auto_ptr2 클래스)에는 사용을 위험하게 만드는 여러 문제가 있습니다.

첫째, std::auto_ptr은 copy constructor와 assignment operator를 통해 move semantics를 구현하기 때문에 std::auto_ptr을 값으로 함수에 전달하면 리소스가 function parameter로 이동되고 함수의 끝에서 소멸됩니다. (function parameter가 범위를 벗어날 때) 그런 다음 호출자로부터 auto_ptr 인수에 액세스하려고 할 때(전송 및 삭제된 것을 인식하지 못함) 갑자기 널 포인터를 dereference하게 됩니다. 그러면 crash가 발생합니다

둘째, std::auto_ptr은 항상 non-array delete를 사용하여 내용을 삭제합니다. 이는 auto_ptr이 dynamically allocated array에 대해서는 올바르게 작동하지 않는다는 것을 의미합니다. 왜나하면 잘못된 종류의 deallocation을 이용하기 때문입니다. 설상가상으로 dynamically allocated array을 전달하는 것을 방지하지 못하므로 관리를 잘못하여 메모리 누수가 발생합니다.

마지막으로 auto_ptr은 대부분의 컨테이너와 알고리즘을 포함하여 표준 라이브러리의 다른 많은 클래스와 잘 어우러지지 않습니다. 이는 표준 라이브러리 클래스가 항목을 복사할 때 이동이 아니라 실제로 복사한다고 가정하기 때문에 발생합니다.

위에서 언급한 단점 때문에 std::auto_ptr은 C++11에서 deprecated(더 이상 사용되지 않으며) C++17에서 제거되었습니다.

profile
청룡동거주민

0개의 댓글