C++ Chapter 15.5 : 스마트 포인터1️⃣ std::unique_ptr

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!


chapter 15. 의미론적 이동과 스마트 포인터

15.5 스마트 포인터1️⃣ std::unique_ptr

🔔 스마트 포인터란?

#include <memory.h> 해주어야 사용 가능하다.

  • 스마트 포인터
    • 👉 포인터가 참조하고 있는 동적 메모리를 자동으로 delete 시켜준다. scope를 벗어나면 알아서 소멸자를 호출해주기 때문!
      • 모든 스마트 포인터는 공통적으로 이 특징을 가짐.
      • 프로그래머가 직접 delete를 명시해줄 필요가 없다.
        • 메모리 누수를 방지해준다.
          • if-else문에 만나거나 예외를 만나 throw 되는 등등 일찍 return 되어 delete문을 만나지 못하는 경우를 방지함
    • ->, * 연산도 오버로딩 되어 있기 때문에 일반 포인터처럼 사용이 가능하다.
  • 스마트 포인터의 종류
    1. unique_ptr
    2. shared_ptr
    3. weak_ptr
  • 선언할 때 일반 포인터처럼 *을 붙이지 않는다.
    Resource * res = new Resource(1000000);  // 일반 포인터 선언과 정의
    std::unique_ptr<Resource> res(new Resource(1000000)); // 스마트 포인터 선언과 정의
    


🔔 unique_ptr

  • 특정 객체에 유일한 소유권(unique)을 부여하는 포인터 객체
    • 포인터가 가리키고 있는 데이터의 소유권이 한 곳에만 속할 경우 사용하는 스마트 포인터.
    • 이 객체를 잘 보관하고 막아주겠다는 성격이 강함
#include <iostream>
#include <memory>  // ⭐⭐
using namespace std;

class Resource
{
public:
	int * m_data = nullptr;
	unsigned m_length = 0;

public:
	Resource()
	{
		cout << "Resource constructed" << endl;
	}

	Resource(unsigned length) 
	{
		cout << "Resource length constructed" << endl;
		this->m_data = new int[length];
		this->m_length = length;
	}
	
	Resource(const Resource &res) // 복사 생성자 
	{
		cout << "Resource copy constructed" << endl;

		Resource(res.m_length);

		for (unsigned i = 0; i < m_length; ++i)  // 깊은 복사
			m_data[i] = res.m_data[i];
	}
	
	~Resource()  // 소멸자
	{
		cout << "Resource destroyed" << endl;
	}
	
	Resource & operator = (Resource & res)  // 대입 연산자 오버로딩
	{
		cout << "Resource copy assignment" << endl;

		if (&res == this) return *this; 

		if (this->m_data != nullptr) delete[] m_data; 

		m_length = res.m_length; 

		m_data = new int[m_length]; 
		for (unsigned i = 0; i < m_length; ++i) 
			m_data[i] = res.m_data[i]; //  깊은 복사

		return *this;
	}
	
	void print()  // Resource의 동적 배열 m_data의 모든 원소값을 출력한다.
	{
		for (unsigned i = 0; i < m_length; ++i)
				std::cout << m_data[i] << " ";
		std::cout << std::endl;
	}

	void setAll(const int& v) // Resource의 동적 배열 m_data 의 모든 원소값을 v 값으로 설정한다.
	{
		for (unsigned i = 0; i < m_length; ++i)
				m_data[i] = v;
	}
};

int main()
{
	{
		std::unique_ptr<int> upi(new int);  // ⭐int 인스턴스를 가리키는 스마트 포인터 upi
	
		auto *ptr = new Resource(5);
		std::unique_ptr<Resource> res1(ptr); // ⭐Resource 타입의 인스턴스를 가리키는 스마트 포인터 res1

		res1->setAll(5); // 모든 원소를 5 로 설정
		res1->print();  // 모든 원소 출력
	}
}
💎출력💎

Resource length constructed
5 5 5 5 5
Resource destroyed


unique_ptr의 함수들

make_unique 함수

std::make_unique 함수 👉 unique_ptr 인스턴스를 안전하게 생성할 수 있다.

  1. 전달 받은 인수를 사용하여 지정된 타입의 객체를 생성
  2. 생성된 객체를 가리키는 unique_ptr리턴받을 수 있다.
    • std::unique_ptr<Resource> res1 = new Resource(5) 는 리턴 없이 단순히 unique_ptr를 선언하는 것이 되는 반면에 std::make_unique 함수를 사용하면 unique_ptr를 리턴 받을 수 있다.
int main()
{
	{
		std::unique_ptr<int> upi(new int);
	
		auto res1 = std::make_unique<Resource>(5);
        // std::unique_ptr<Resource> res1 = new Resource(5);  

		res1->setAll(5);
		res1->printf();

	}
}
  • auto res1 = std::make_unique<Resource>(5)
    1. new Resource(5) 하여 생성자를 호출하고 객체를 생성한 후
    2. 이 객체를 가리키는 unique_ptr를 리턴하여 res1에 복사된다. 👉 얕은 복사

get 함수

get() unique_ptr 자체에서 가지고 있는 함수로 일반 포인터를 리턴시킨다.

void doSomething2(Resource * res)  // 일반포인터
{
	res->setAll(100);
	res->print();
}

int main()
{
    auto res1 = std::make_unique<Resource>(5);
    doSomething2(res1.get());
}
  • res1.get()
    • unique_ptr인 res1을 일반 포인터로서 리턴하여 doSomething2 함수에게 인수로 넘기고 있다.


std::unique_ptr의 특징

// res1, res2는 unique_ptr 타입의 스마트 포인터

auto res1 = std::make_unique<Resource>(5);
std::unique_ptr<Resource> res2;

res2 = res1; // ❌오류! unique_ptr은 복사를 못한다.
res2 = std::move(res1); // ⭕
  • 복사를 못 한다. Copy Semantics는 안됨. 컴파일 오류 남!
    • unique_ptr은 ✨한 객체의 소유권은 오로지 한 곳에서만 가질 수 있기 때문에 res2 = res1 이렇게 포인터끼리 단순히 복사를 하면 res2res1 두 곳에서 동일한 인스턴스에 대해 소유권을 가지게 되기 때문에!
  • 이동만 할 수 있다. Move Semantics만 사용 가능.
    • res2 = std::move(res1)
      • res1이 R-value로 바뀐다.
      • res1은 소유권이 박탈 되어 이제 아무 객체도 가리키지 않는 nullptr이 되고
      • res2res1이 소유하고 있던 객체의 소유권을 물려 받게 된다.
    • res1은 소유권이 박탈되고 그 소유권이 res2로 이전되므로 한 객체의 소유권이 오로지 한 곳에서만 가질 수 있다는 unique_ptr의 성질이 보장된다.

이처럼 unique_ptr은 서로의 단순 복사를 막아 어떤 객체에 대한 소유권을 오로지 하나의 unique_ptr에서만 가리킬 수 있도록 보장해준다.

auto doSomething()
{
	return std::unique_ptr<Resource>(new Resource(5));
    // return std::make_unique<Resource>(5);
}

int main()
{
	{
		
        auto res1 = std::make_unique<Resource>(5);
		// auto res1 = doSomething(); 

		res1->setAll(5);
		res1->print();
	
		std::unique_ptr<Resource> res2; // unique_ptr인 res2는 선언만 됐고 아직 가리키고 있는 객체는 없으므로 nullptr인 상태

        // std::boolalpha : 0, 1 대신 true, false 출력. null이면 false출력.
		std::cout << std::boolalpha;  
		std::cout << static_cast<bool>(res1) << std::endl; // true 출력
		std::cout << static_cast<bool>(res2) << std::endl; // false 출력

		res2 = std::move(res1);

		std::cout << std::boolalpha;
		std::cout << static_cast<bool>(res1) << std::endl; // false 출력. res1은 소유권을 잃어 nullptr이 됨
		std::cout << static_cast<bool>(res2) << std::endl; // true 출력. res2에게로 소유권이 이전됨

		if (res1 != nullptr) res1->print();
		if (res2 != nullptr) res2->print();
	}
}
💎출력💎

Resource length constructed
5 5 5 5 5
true
false
false
true
5 5 5 5 5
  • 함수 리턴 값은 R-value이니까 unique_ptr를 리턴하는 doSomething()unique_ptr를 R-value로서 리턴하는 것이나 마찬가지.
    • auto res1 = doSomething();
      • 임시 객체 리턴의 소유권이 res1로 이전
  • auto res1 = std::make_unique<Resource>(5);
    • res1은 현재 Resource 객체를 가리키고 있는 중
  • std::unique_ptr<Resource> res2;
    • unique_ptr인 res2선언만 됐고 아직 가리키고 있는 객체는 없으므로 nullptr인 상태
      • 정의까지 하려면 std::unique_ptr<Resource> res2 = new Resource(5); 가 됐었어야 함
  • res2 = std::move(res1);
    • res1이 가리키던 객체의 소유권이 res2에게로 이전 되었기 때문에 res1은 소유권이 박탈 되어 nullptr이 된다.
Resource * res = new Resource;
std::unique_ptr<Resource> res1(res);
std::unique_ptr<Resource> res2(res);   // ❌에러

delete res;  // ❌불상사 발생 가능성 
  • 일반 포인터 res가 가리키고 있는 객체를 unique_ptr인 res1res2가 동시에 소유하려고 하니 오류가 발생한다.
    • unique_ptr은 한 객체에 대한 소유권이 한 포인터에만 있어야하니까 이와 같은 상황도 방지해주는 것!
  • 스마트 포인터인 unique_ptr은 scope를 벗어나면 알아서 자동으로 delete되기 때문에 이렇게 프로그래머가 delete을 명시해주는게 컴파일 오류가 되는건 아니지만 큰 문제가 생길 수 있다. 이미 delete되었는데 또 delete하려는 시도가 될 수 있어서!


unique_ptr을 함수 파라미터로 넘겨주는 경우

void doSomething2(std::unique_ptr<Resource> & res) 👉 unique_ptr을 함수 파라미터로 넘겨 받을 때 레퍼런스로 받을 것을 권장한다.

void doSomething2(std::unique_ptr<Resource> & res)  // ✨ unique_ptr 를 인수로 받을 땐 레퍼런스로 받기를 권장함
{                                                  
	res->setAll(10); 
}

int main
{
	{
		auto res1 = std::make_unique<Resource>(5);
		res1->setAll(1);
		res1->print();

		doSomething2(res1); // ✨ unique_ptr인 res1을 매개변수 res가 참조하게 된다.

		res1->print();
	}
}

unique_ptr reference 가 아닌 그냥 unique_ptr로 받으면 컴파일 오류가 난다.

  • unique_ptr 은 가리키는 객체에 대해 소유권이 유일 해야 해서 '복사'를 금지하기 때문이다.
    • 두 unique_ptr이 한 객체에 대한 소유권을 동시에 가지 우려가 있기 때문
void doSomething2(std::unique_ptr<Resource> res)  // 레퍼런스가 아닌 그냥 unique_ptr으로 받으려고 하고 있다.
{                                                   
	res->setAll(100);  
}

int main
{
	{
		auto res1 = std::make_unique<Resource>(5);
		res1->setAll(1);
		res1->print();

		doSomething2(res1);  // ❌컴파일 오류 발생. res = res1 복사를 금지하기 때문에.

		res1->print();
	}
}

unique_ptr 을 인수로 받을 때 레퍼런스로 받지 않고 일반 unique_ptr로 받을거라면 인수를 std::move로 넘겨 소유권을 이전+박탈 하여 받아보자.

void doSomething2(std::unique_ptr<Resource> res)  // 레퍼런스가 아닌 그냥 unique_ptr으로 받으려고 하고 있다.
{                                                   
	res->setAll(100);  
	res->print();
}

int main
{
	{
		auto res1 = std::make_unique<Resource>(5);
		res1->setAll(1);
		res1->print();

		std::cout << std::boolalpha;
		std::cout << static_cast<bool>(res1) << std::endl; // true 출력

		doSomething2(std::move(res1));  // std::move 로 res1의 소유권을 res로 이전한다. 그러나 res1의 소유권은 박탈되어 nullptr이 되버린다는 문제가 생김
		
		std::cout << std::boolalpha;
		std::cout << static_cast<bool>(res1) << std::endl; // false 출력. res1가 nullptr이 되어 버림
	}
}
  • res = std::move(res1)이 되는 것이나 마찬가지.
    • 객체의 소유권이 res1에서 res로 옮겨가는 것이니 유일한 소유권이 보장되어 문제 없다.
    • 그러나 이렇게 되면 res1의 소유권이 박탈되어 nullptr이 된다는 문제가 생긴다.
      • 또한 res가 함수의 매개변수이기 때문에 함수가 끝나면 해당 객체가 delete되어버린다.

res1이 원래 소유했던 객체를 잃고 싶지 않다면 그 객체의 소유권을 이전 받은 res2가 함수가 종료되어 delete되기 전에 res1에게 다시 그 객체를 리턴해주면 된다!

auto doSomething2(std::unique_ptr<Resource> res) // 레퍼런스가 아닌 그냥 unique_ptr으로 받으려고 하고 있다.
{                                                   
	res->setAll(100);  
	res->print();

	return res;  // ✨객체를 다시 리턴한다.
}


int main
{
	{
		auto res1 = std::make_unique<Resource>(5);
		res1->setAll(1);
		res1->print();

		std::cout << std::boolalpha;
		std::cout << static_cast<bool>(res1) << std::endl; // true 출력

		res1 = doSomething2(std::move(res1));  // res1의 소유권이 박탈되나 소유권이 박탈된 그 객체를 다시 리턴받으므로써 문제 없게 된다!
		
		std::cout << std::boolalpha;
		std::cout << static_cast<bool>(res1) << std::endl; // true 출력
	}
}

  • res1res에게 소유권을 넘겨주어 소유권이 박탈되었지만 res1가 함수가 끝날 무렵에 다시 그 객체를 res1에게 리턴해주어 문제가 해결된다!

get()함수를 사용하여 일반 포인터로 잠시 변환하여 넘길 수도 있다.

void doSomething2(Resource * res)  // 일반포인터
{
	res->setAll(100);
	res->print();
}

int main()
{
    auto res1 = std::make_unique<Resource>(5);
    doSomething2(res1.get());
}
  • res1.get()
    • unique_ptr인 res1을 unique_ptr이 아닌 일반 포인터로서 넘기고 있기 때문에 resres1은 같은 객체를 가리키게 되더라도 문제가 없다.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기