KoreanFoodie's Study

C++ 기초 개념 13-1 : RAII 패턴과 unique_ptr 본문

Tutorials/C++ : Beginner

C++ 기초 개념 13-1 : RAII 패턴과 unique_ptr

GoldGiver 2022. 4. 19. 10:23

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

 

Resource Acquisition Is Initialization - RAII

자원의 획득은 초기화다. 이는 자원 관리를 스택에 할당한 객체를 통해 수행하는 것이다.

함수 내부에서 예외가 발생해서 소멸자가 호출하는 코드에 도달하지 못하더라도, 해당 함수의 스택에 정의되어 있는 모든 객체들은 빠짐없이 소멸자가 호출된다(stack unwinding).

만약 이 소멸자들 안에 다 사용한 자원을 해제하는 루틴을 넣으면 어떨까?

 

A* pa = new A();

예를 들어, 위의 포인터 pa의 경우, 객체가 아니므로 소멸자가 호출되지 않는다. 그 대신, pa를 일반적인 포인터가 아닌, 포인터 '객체'로 만들어서 소멸될 때 자신이 가리키고 있는 객체가 delete 되도록하게 만들면, 자원(메모리) 관리가 스택의 객체(포인터 객체)를 통해 수행되게 된다.

 

이런 방식의 포인터 객체를 스마트 포인터(smart pointer)라고 한다. C++11 부터는 auto_ptr를 대체하는 unique_ptr와 shared_ptr를 제공하고 있다.

 

 

객체의 유일한 소유권 - unique_ptr

아래와 같은 코드에서 발생하는 문제를 double free 버그라고 한다.

Data* data = new Data();
Date* data2 = data;

// data 의 입장 : 사용 다 했으니 소멸시켜야지.
delete data;

// ...

// data2 의 입장 : 나도 사용 다 했으니 소멸시켜야지
delete data2;

data2는 data를 가리키는데, data는 이미 소멸되어 data2를 소멸시키는 시도를 하다가 프로그램이 죽는 것이다.

이 경우, unique_ptr를 이용해 data에 new Data( )로 생성된 객체의 소유권을 준다면, delete data만 가능하도록 만들 수 있다.

 

#include <iostream>
#include <memory>

class A {

int *data;

public:
	A() {
		std::cout << "자원을 획득함!" << std::endl;
		data = new int[100];
	}

	void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

	~A() {
		std::cout << "자원을 해제함!"	 << std::endl;
		delete[] data;
	}
};

void do_something() {
	std::unique_ptr<A> pa(new A());
	pa->some();
}

int main() { do_something(); }

위의 unique_ptr 문법은, pa 를 마치 A* pa = new A(); 로 정의한 것처럼 사용할 수 있다. 다만 pa가 스택에 정의된 객체이기 때문에, do_something 함수가 종료될때 자동으로 소멸자가 호출된다!

 

만약 unique_ptr 를 복사하려면 어떨까?

std::unique_ptr<A> pa(new A());
std::unique_ptr<A> pb = pa;

이 경우에는 아래와 같이 삭제된 함수를 사용했다는 오류가 나온다.

'std::unique_ptr<A,std::default_delete<_Ty>>::unique_ptr(const std::unique_ptr<_Ty,std::default_delete<_Ty>> &)': attempting to reference a deleted function

 

 

삭제된 함수

C++ 11에는 사용을 원치 않는 함수를 삭제하는 기능이 추가되었다.

class A {
public:
	A(int a) {};
	A(const A& a) = delete;
};


int main() {
	A a(3); // 가능
	A b(a); // 불가능
}

위와 같이, 복사 생성자를 명시적으로 삭제하게 되면, 클래스 A의 복사 생성자는 존재하지 않게 된다.

unique_ptr 도 마찬가지로 unique_ptr 의 복사 생성자가 명시적으로 삭제되었다. 왜냐하면 unique_ptr 는 객체를 유일하게 소유해야 하고, 그렇게 구현이 되어야 double free 같은 버그가 발생하지 않는다.

 

 

unique_ptr 소유권 이전하기

unique_ptr 는 복사가 되지 않지만, 소유권은 이전할 수 있다.

void do_something() {
	std::unique_ptr<A> pa(new A());

	std::unique_ptr<A> pb = std::move(pa);
}

unique_ptr 은 복사 생성자는 정의되어 있지 않지만, 이동 생성자는 가능하다. 실제로, move 이후 pa.get() 을 통해 pa 가 가리키고 있는 실제 주소값을 확인해 보면 0 (nullptr)이 나온다.

소유권이 이전된 unique_ptr는 댕글링 포인터(dangling pointer)라고 하며 재참조할 시에 런타임 오류가 발생한다.

 

 

unique_ptr를 함수 인자로 전달하기

unique_ptr 는 복사 생성자가 없다. 만약 그냥 함수에 레퍼런스로 전달하면 어떨까?

void A::do_sth(int a) {
	std::cout << "Do something" << std::endl;
	data[0] = a;
}

// 올바르지 않은 전달 방식
void do_something(std::unique_ptr<A>& ptr) {
	ptr->do_sth(1);
}

int main() {
	std::unique_ptr<A> pa(new A());
	do_something(pa); 
}

하지만 위의 방식으로 unique_ptr를 전달하게 되면, do_something 함수 내부의 ptr도 unique_ptr 의 객체에 접근할 수 있으므로, 유일한 소유권 원칙을 위배하게 된다. 따라서 unique_ptr 를 함수의 인자로 전달할 때는 원래의 포인터 주소값을 전달해 주어야 한다.

 

void do_something(A* ptr) {
	ptr->do_sth(1);
}

int main() {
	std::unique_ptr<A> pa(new A());
	do_something(pa.get()); 
}

unique_ptr 의 get 함수를 호출하면, 실제 객체의 주소값을 리턴해 준다. do_something 의 ptr 은 일반적인 포인터를 받는데, 이렇게 되면 소유권을 가지지 않은 채 함수 내부에서 객체에 겁근할 수 있는 권한을 부여받는 것이다.

 

3줄 요약 :

unique_ptr 는 객체의 유일한 소유권을 가진 포인터이다.

unique_ptr 를 함수의 인자로 전달할 때에는 get 을 이용해 실제 주소값을 전달하면 된다.

소유권을 이전할 때는 move 를 이용한다.

 

 

unique_ptr 쉽게 생성하기

C++ 14부터 unique_ptr 을 간단히 만들 수 있는 std::make_unique 함수를 제공한다.

auto ptr = std::make_unique<A>();

make_unique 함수는 템플릿 인자로 전달된 클래스의 생성자의 인자들에 완벽한 전달을 수행한다. 따라서

auto ptr = std::make_unique<A>(new A());

와 같이 불필요한 생성자를 호출할 필요가 없다.

 

 

unique_ptr 를 원소로 가지는 컨테이너

unique_ptr는 복사 생성자가 없어, 사용에 유의가 필요하다.

int main() {

	std::vector<std::unique_ptr<A>> vec;
	std::unique_ptr<A> pa(new A(1));

	vec.push_back(std::move(pa));
}

vector 의 push_back 함수는 복사 생성자를 호출하므로, unique_ptr 를 삽입할 때는 move 함수를 사용해 주어야 한다.

 

혹은, emplace_back 함수를 사용해서 unique_ptr 를 직접 생성하면서 삽입할 수 있다.

vec.emplace_back(new A(1));

emplace_back 함수는 전달된 인자를 완벽한 전달(perfect forwarding)을 통해, unique_ptr<A> 의 생성자에 전달해서, vector 맨 뒤에 생성해 move 를 사용했던 때처럼 불필요한 이동 연산을 줄인다.

 

emplace_back을 사용시, 어떠한 생성자가 호출되는지 주의해야 한다.

// v1에 int 값 1000의 원소 삽입
std::vector<int> v1;
v1.emplace_back(1000);

// v2에 1000개 짜리 배열의 벡터가 삽입
std::vector<std::vector<int>> v2;
v2.emplace_back(1000);

 

Comments