M.6 std::unique_ptr

주홍영·2022년 3월 24일
0

Learncpp.com

목록 보기
196/199

https://www.learncpp.com/cpp-tutorial/stdunique_ptr/

우리는 챕터 초반부에 pointer의 사용이 어떻게 bug와 memory leak을 발생시킬 수 있는지 살펴봤다
예를들어 function을 의도치 않게 early return 한 경우에 pointer가 제대로 해제가 되지 않을 수 있다

#include <iostream>

void someFunction()
{
    auto* ptr{ new Resource() };

    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x;

    if (x == 0)
        throw 0; // the function returns early, and ptr won’t be deleted!

    // do stuff with ptr here

    delete ptr;
}

위의 function을 보면 function scope를 벗어나고 ptr이 해제가 되지 않아서 memory leak발생

move semantics의 기초를 다루었으므로 이제 스마트 포인터 클래스에 대해서 다뤄보자.
참고로 스마트 포인터는 dynamically 할당된 object를 관리하는 클래스입니다.
비록 스마트 포인터가 다른 특성도 제공할 수 있지만
스마트 포인터의 핵심 특성은 dynamically allocated된 resource나 object의 해제를 적절한 타이밍에 이뤄지도록 하는 것이 주 목표이다

이러한 이유로 스마트 포인터는 절대로 그들 자체로는 동적할당해서 사용하지 않는다
stack에 할당해서 사용해야지 우리는 스마트 포인터로 동적할당을 올바르게 사용한다고 보장할 수 있다

c++11은 4개의 smart pointer를 제공한다

  • std::auto_ptr (사용하지 말아야한다. c++17에서는 삭제됨)
  • std::unique_ptr (가장 많이 사용되는 스마트 포인터 클래스)
  • std::shared_prt
  • std::weak_ptr

std::unique_ptr

std::unique_ptr은 c++11에서 std::auto_ptr을 대체하기 위한 클래스이다
uniuqe_ptr은 반드시 하나의 object에서만 쓰여야 한다, 복수의 object가 아닌
이말은 unique_ptr의 경우 온전히 하나의 객체만이 ownership을 가지고 있고
다른 object와 ownership을 공유할 수가 없다
std::unique_ptr은 < memory > header에 정의되어 있다

간단한 예시를 통해 살펴보자

#include <iostream>
#include <memory> // for std::unique_ptr

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

int main()
{
	// allocate a Resource object and have it owned by std::unique_ptr
	std::unique_ptr<Resource> res{ new Resource() };

	return 0;
} // res goes out of scope here, and the allocated Resource is destroyed

위의 프로그램에서 std::unique_ptr타입의 res는 stack에 할당되어 있으므로
main함수의 scope를 벗어나면 소멸한다
이때 자동으로 할당된 Resource가 해제된다

std::auto_ptr과는 다르게 std::unique_ptr은 move semantics가 적절하게 구현되어 있다

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

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

int main()
{
	std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
	std::unique_ptr<Resource> res2{}; // Start as nullptr

	std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
	std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");

	// res2 = res1; // Won't compile: copy assignment is disabled
	res2 = std::move(res1); // res2 assumes ownership, res1 is set to null

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

	std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
	std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");

	return 0;
} // Resource destroyed here when res2 goes out of scope

위으 프로그램은 다음의 출력을 준다

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

std::unique_ptr은 move semantics를 염두해 두고 설계되었기 때문에 copy initialization과 copy assignment가 불가능하다.
만약 unique_ptr로 관리되고 있는 content를 옮기고 싶으면 무조건 move semantics를 활용한다
위의 프로그램에서 우리는 std::move를 통해 이를 달성하고 있다
std::move는 res1을 r-value로 타입 변환을 시켜주고 있고
이는 copy assignment대신에 move assignment를 발동시키고 있다

Accessing the managed object

std::unique_ptr은 overloaded operator* 와 operator->가 구현되어 있다
이는 관리하고 있는 content를 참조할 수 있게 해준다

operator* 는 관리하고 있는 resource의 reference를 반환해주고
operator->는 관리하고 있는 resource의 pointer를 반환해준다

한가지 기억할 것은 std::unique_ptr은 항상 object를 관리하고 있는 것이 아니다
그리고 object가 없는 empty상태로도 instantiate이 가능하다는 것을 알고 있다
더불어 다룬 unique ptr에 ownership을 넘겨주는 경우에도 nullptr을 들고 있을 수 있다
우리는 앞서말한 두 operator를 사용하기 전에 unique_ptr이 empty가 아닌지 체크를 해야한다
다행히도 std::unique_ptr은 bool로 casting하면 true or false로 empty인지 아닌지 알 수 있다

여기 예시가 있다

#include <iostream>
#include <memory> // for std::unique_ptr

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
	friend std::ostream& operator<<(std::ostream& out, const Resource &res)
	{
		out << "I am a resource\n";
		return out;
	}
};

int main()
{
	std::unique_ptr<Resource> res{ new Resource{} };

	if (res) // use implicit cast to bool to ensure res contains a Resource
		std::cout << *res << '\n'; // print the Resource that res is owning

	return 0;
}

위의 프로그램에서 if(res)를 통해서 implicit type casting을 이용해서 체크를 하고 있다
출력은 다음과 같다

Resource acquired
I am a resource
Resource destroyed

위 프로그램에서 operator* 을 이용해 Resource 클래스 object를 reference한 다음에
operator<< overloading으로 cout을 이용해 바로 출력하고 있다

그리고 scope를 벗어나서 destroyed되는 모습까지 출력으로 확인가능하다

std::unique_ptr and arrays

std::auto_ptr과는 다르게 std::unique_ptr은 scalar delete와 array delete를 모두 사용할 수 있을만큼 똑똑한 클래스이다.

그러나 std::array 혹은 std::vector (or std::string)이 대부분 경우에 std::unique_ptr과 fixed arrayr혹은 dynmaic array, c-style string을 섞어서 쓰는 것보다 더 나은 선택이다

c++14에는 std::make_unique()라는 함수가 추가되었다

#include <memory> // for std::unique_ptr and std::make_unique
#include <iostream>

class Fraction
{
private:
	int m_numerator{ 0 };
	int m_denominator{ 1 };

public:
	Fraction(int numerator = 0, int denominator = 1) :
		m_numerator{ numerator }, m_denominator{ denominator }
	{
	}

	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
	{
		out << f1.m_numerator << '/' << f1.m_denominator;
		return out;
	}
};


int main()
{
	// Create a single dynamically allocated Fraction with numerator 3 and denominator 5
	// We can also use automatic type deduction to good effect here
	auto f1{ std::make_unique<Fraction>(3, 5) };
	std::cout << *f1 << '\n';

	// Create a dynamically allocated array of Fractions of length 4
	auto f2{ std::make_unique<Fraction[]>(4) };
	std::cout << f2[0] << '\n';

	return 0;
}

위와 같이 사용할 수 있다
make_unique() function은 optional이지만 직접 만들어서 쓰는 것보다는 make_unique를 사용하는 것을 추천한다

Best practice

Use std::make_unique() instead of creating std::unique_ptr and using new yourself.

The exception safety issue in more detail

exception에 대해서 다루지 않아서 일단은 넘어간다

Passing std::unique_ptr to a function

만약 우리가 function이 ownership을 가져가게 하고 싶다면 std::unique_ptr을 pass by value를 이용해야 한다. 참고로 copy semantics가 비활성화 되어있기 때문에 우리는 std::move를 이용해서 argument를 r-value로 만들어야 한다

#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
	friend std::ostream& operator<<(std::ostream& out, const Resource &res)
	{
		out << "I am a resource\n";
		return out;
	}
};

void takeOwnership(std::unique_ptr<Resource> res)
{
     if (res)
          std::cout << *res << '\n';
} // the Resource is destroyed here

int main()
{
    auto ptr{ std::make_unique<Resource>() };

//    takeOwnership(ptr); // This doesn't work, need to use move semantics
    takeOwnership(std::move(ptr)); // ok: use move semantics

    std::cout << "Ending program\n";

    return 0;
}

위 프로그램을 보면 ptr을 std::move를 통해 r-value로 만들어서 pass by value하고 있다
이렇게 하면 function이 paramter res를 통해 ownership을 전달받게 되는 것이다
그리하여 function의 scope가 끝나면서 Resouce가 파괴된다
따라서 출력은 다음과 같다

Resource acquired
I am a resource
Resource destroyed
Ending program

그러나 대부분의 경우에 우리는 function이 resource의 ownership을 가져가게 하지는 않는다
비록 우리는 pass by ref를 이용해 전달할 수 있을지라도, function에서 resource에 수정을 가하는 경우에만 pass by ref를 이용해야한다

그렇지 않은 경우 resource 그 자체를 ref나 pointer로 넘겨주는 것이 더 나은 선택이다
이렇게 한다면 function은 caller가 어떻게 resource를 관리하고 있는지 모르게 할 수 있다
std::unique_ptr의 raw pointer를 사요하고 싶으면 get()이라는 member function이 있다

#include <memory> // for std::unique_ptr
#include <iostream>

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

	friend std::ostream& operator<<(std::ostream& out, const Resource &res)
	{
		out << "I am a resource\n";
		return out;
	}
};

// The function only uses the resource, so we'll accept a pointer to the resource, not a reference to the whole std::unique_ptr<Resource>
void useResource(Resource* res)
{
	if (res)
		std::cout << *res << '\n';
}

int main()
{
	auto ptr{ std::make_unique<Resource>() };

	useResource(ptr.get()); // note: get() used here to get a pointer to the Resource

	std::cout << "Ending program\n";

	return 0;
} // The Resource is destroyed here

따라서 위 프로그램은 resource의 pointer만 넘겨주게 된다
출력은 다음과 같다

Resource acquired
I am a resource
Ending program
Resource destroyed

std::unique_ptr and classes

당연히 우리는 std::unique_ptr을 우리가 설계하는 class의 member로 사용할 수 있다
이 경우에 우리는 우리의 클래스 destructor가 dynamic memory를 해제하는 것에 걱정할 필요하 없다. std::unique_ptr은 클래스 object이므로 클래스가 파괴되면 자연스래 함께 파괴되기 때문이다. 그러나 우리가 설계한 class object가 dynamically allocating 되었을 때는 당연히 이야기가 다르다.

Misusing std::unique_ptr

unique_ptr을 사용하는데 두가지 잘못된 방식이 있다
첫째, 절대로 복수의 unique_ptr이 동일한 resource를 관리하도록 만들지 말라

Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };

위와 같은 경우 실행은 되겠지만 프로그램 말미에 res를 두 번 해제하려고 하기 때문에 문제가 생긴다

둘째, 절대로 std::unique_ptr object를 수동으로 delete하지 말라

Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;

만약 그렇게 한다면 똑같이 복수 해제 문제가 발생한다

profile
청룡동거주민

0개의 댓글