본문 바로가기

Information Technology/C++

[C++] std::unique_ptr(1)

이 글은 개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;)


2019/11/15 - [프로그래밍/C++] - [C++] std::move(1)

 

[C++] std::move(1)

개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;) #pragma once #include template class AutoPtr { public: T* m_ptr; public: AutoPtr(T* ptr = nullptr) :m_ptr(ptr)..

movahws.tistory.com

헤더 파일은 위의 포스팅에서 사용한 Resource.h를 사용하고 있습니다!


이번 포스팅에서는 하나의 주소에 저장된 값을 단 하나의 포인터에서만 저장하고 관리하는 std::unique_ptr에 대해 알아보겠습니다.

#include <iostream>
#include <memory>
#include "Resource.h"
using namespace std;

int main()
{
    Resource *res = new Resource(100000);
    // throw or early return?? -> memory leak
    delete res;
}

여기 헤더 파일에 정의된 Resource 클래스의 포인터 자료형 인스턴스 res를 동적 할당을 통해 정의했습니다.

동적 할당은 메모리 누수(memory leak)를 방지하기 위해 반드시 delete 선언을 해줘야 하고요.

그런데 만약 선언된 res가 중간에 예외처리로 인해 throw 되거나 예상치 못하게 early return 된다면, delete 구문이 실행되지 않고 넘어가기 때문에 메모리 누수가 발생하게 됩니다.

이를 막기 위한 것이 바로 std::unique_ptr입니다.

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

unique_ptr의 사용방법은 위의 코드 블록과 같습니다. 템플릿 함수 혹은 템플릿 클래스를 사용할 때처럼,

unique_ptr 선언 뒤에 사용할 자료형을 입력합니다. 예제에서는 Resource 클래스를 사용하고 있습니다. int, float과 같이 사전 정의 자료형도 물론 사용 가능합니다.

그다음에 변수명을 정해주고, 정의하려는 자료형의 인스턴스를 new를 통해 동적 할당해줍니다. 그리고 실행을 해보면

delete를 통해 소멸자를 호출하지 않았음에도 unique_ptr에서 알아서 소멸자를 호출하는 걸 볼 수 있습니다.

이게 unique_ptr에 대한 개략적인 소개입니다. 이제 더 자세한 unique_ptr에 대한 사용법을 알아보겠습니다.


우선 유니크 포인터를 선언하는 방법은 위의 방법 외에도 다양하게 존재합니다.

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

그중 하나가 위와 같이 make_unique를 사용하는 방법입니다.

int main()
{
    std::unique_ptr<Resource> res1 = std::make_unique<Resource>(5);
}

자료형 선언을 위와 같이 해야 하지만 auto를 통해 자료형 선언은 컴파일러에게 맡겼습니다.

auto doSomething()
{
	return std::make_unique<Resource>(5);
	// 함수를 통해 리턴되는 값도 unique_ptr을 통해 move semantics
}

int main()
{
	auto res1 = doSomething();
}

아니면 위와 같이 doSomething 함수와 같이 유니크 포인터를 사용한 인스턴스를 리턴하는 함수를 만들어서,

그 리턴값으로 res1을 생성해도 됩니다.

여기서 알아두셔야 할 점은, doSomething을 통해 리턴되는 Resource 자료형의 인스턴스는 unique_ptr을 사용하기 때문에 deep copy가 아닌 move semantics의 방식으로 res1에 전달된다는 점입니다.

    int main()
    {
		//std::unique_ptr<Resource> res1(new Resource(5));
		//auto res1 = std::make_unique <Resource>(5);
		auto res1 = doSomething();
		res1->setAll(5); // 배열을 전부 5로 채움
		res1->print(); // 5 5 5 5 5 출력

		std::unique_ptr<Resource> res2; // default constructor를 실행. res2의 주소는 nullptr
		cout << std::boolalpha;
		cout << static_cast<bool>(res1) << endl; // 주소가 존재하므로 True 출력
		cout << static_cast<bool>(res2) << endl; // 주소가 아직 nullptr이므로 False 출력

		cout << endl;

		// res2 = res1; 하나의 주소에 대하여 한 개의 인스턴스만 소유할 수 있으므로 copy semantics 사용 불가능
		res2 = std::move(res1); // 주소에 대한 소유권을 완전히 넘겨주는 move semantics 사용만 가능
		cout << static_cast<bool>(res1) << endl;
		cout << static_cast<bool>(res2) << endl;
		
		if (res1 != nullptr) res1->print();
		if (res2 != nullptr) res2->print(); // 5 5 5 5 5 출력
	}

위의 코드를 그대로 가져와서 unique_ptr에 대해 더 설명해보겠습니다.

Resource 클래스 인스턴스인 res1의 멤버인 setAll함수를 통해 배열을 전부 5로 채웠고,

print함수를 통해 배열의 값을 출력했습니다.

그 밑에 unique_ptr을 사용해서 Resource 클래스의 인스턴스인 res2를 선언했습니다.

그리고 static_cast<bool>을 사용해서 인스턴스의 주소가 존재한다면 True를, 인스턴스의 주소가 nullptr이라면 False를 출력하는 출력문을 입력했습니다.

res1은 5로 채워진 배열을 가지고 있기 때문에 True를 출력하지만, res2는 default constructor를 통해 생성되었을 뿐 아직 주소를 할당받지 못했기 때문에 False를 출력합니다.

res2에도 주소를 주기 위해 밑에서 std::move를 사용해서 res1의 주소를 res2에게 완전히 넘겨줬습니다.

unique_ptr은 말 그대로 하나의 변수만이 그 주소를 소유하고 관리할 수 있기 때문에 res2 = res1과 같이 copy semantics를 사용하는 방식으로 res1의 주소를 res2에게도 줄 수 없습니다.

대신 주소의 소유권을 완전히 넘겨주는 std::move를 통해서 res1의 주소를 res2에게 넘겨줄 수 있습니다.

res1은 std::move를 통해 자신의 주소를 res2에게 넘겨주게 되고 자신은 nullptr이 됩니다.

그리고 다시 이전과 같은 출력문을 입력하게 되면,

res1은 nullptr이기 때문에 False를 출력하고 res2는 res1의 주소를 넘겨받아 주소가 존재하므로 True를 출력합니다.

그 밑에 있는 조건문 역시 주소가 nullptr가 아닌 res2의 배열, 즉 res1에 저장되었던 5로 채워진 배열을 출력합니다.

이게 최종 출력값입니다. 위에서도 말씀드린 것처럼 copy semantics를 사용하지 않고 move semantics를 사용했기 때문에 프로그램 전체에서 생성자와 소멸자가 각각 1번만 호출되는 걸 알 수 있습니다.

또 다른 사용법은 다음 포스팅에서 다루겠습니다.

 

'Information Technology > C++' 카테고리의 다른 글

[C++] std::shared_ptr  (0) 2019.12.04
[C++] std::unique_ptr(2)  (0) 2019.12.02
[C++] STL의 반복자 iterator  (0) 2019.11.30
[C++] 표준 템플릿 라이브러리, 컨테이너 소개  (0) 2019.11.30
[C++] 예외 클래스와 상속  (0) 2019.11.25