RAII 디자인 패턴

 

Resource Acquisition Is Initialization의 약자로 자원의 획득은 초기화다 라는 뜻

 

C++ 예외처리 공부할때 배웠던 내용 중 생성자 내에서 예외가 발생하거나, 혹은 delete문 이전에 예외가 발생하는 경우 소멸자가 호출되지 않거나, delete문이 호출되지 않을 수 있는데 이때 스마트포인터를 사용한다고 했음

 

스마트 포인터 사용의 방법은 동적할당을 일반자료형의 포인터가 아닌 포인터 객체로 만들어서 자원을 가리키게 하는 것

그럼 해당 포인터 객체가 소멸될때 소멸자에서 delete를 하게 되면 스택에 있는 객체이므로 반드시 소멸자가 호출 됨

 

C++ 11 이전 버전의 스마트 포인터를 보완해서

modern C++에서 사용하는 스마트 포인터가 바로 unique_ptr과 shared_ptr

 

 

 

unique_ptr

 

특정 객체에 유일한 소유권을 부여하는 포인터 객체를 unique_ptr이라고 함

 

C++에서 메모리를 잘못된 방식으로 관리하면 두가지 문제점이 생길 수 있음

1) 메모리 릭(이건 RAII패턴을 사용해서 해결 가능)

2) 이미 해제된 메모리를 다시 참조하는 경우

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

delete data;
delete data2; // 이미 할당 해제된 객체를 또 소멸시키려 함

위의 코드가 2번 case인데 이미 소멸된 객체를 또 소멸시키려 하면 메모리 오류가 나면서 프로그램이 죽게 됨

이런 버그를 double free 버그라고 부르며 이런 문제가 발생하는 이유는 만들어진 객체의 소유권이 명확하지 않기 때문

 

소유권이란 어떤 포인터에 객체의 유일한 소유권을 부여해서 이 포인터 말고는 객체를 소멸시킬 수 없게 만들면 double free 버그는 일어나지 않게 될 것

위의 경우 data에 new Data()로 객체의 소유권을 부여하면 delete data는 가능하지만 delete data2는 불가능해짐

이 소유권 부여를 unique_ptr을 사용해서 하는 것

#include <iostream>
#include <memory>

class A 
{
	int *data;

 public:
	A() 
	{  
		data = new int[100];
	}

	void some() { std::cout << "일반 포인터처럼 사용\n"; }

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

void do_something() {
	std::unique_ptr<A> pa(new A()); // A pa = new A(); 와 동일
	pa->some();
}

int main() 
{
	do_something(); 
}​

 

즉 생성자에 동적할당이 있으면 unique_ptr로 객체 생성을 해버리고 소멸자에 delete를 선언해두는게 나중에 delete를 고려할 필요가 없어진다.

 

만약 아래처럼 유일한 소유권을 가져가려 하면 삭제된 함수를 사용했다는 컴파일 오류가 발생함

void do_something() {
  std::unique_ptr<A> pa(new A());
  std::unique_ptr<A> pb = pa; // 오류발생
}

 

삭제된 함수란 사용을 원하지 않는 함수를 삭제시키는 방법이며 아래처럼 사용함

#include <iostream>


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

int main() {
  A a(3);  // 가능
  A b(a);  // 불가능 (복사 생성자는 삭제됨)
}

즉 개발자가 명시적으로 이 함수는 사용하지 못하도록 금지할 수 있음

unique_ptr 또한 유일하게 소유권을 가져야하므로 복사생성자 사용이 금지되어 있음

 

 

 

unique_ptr의 소유권 이전

 

void do_something() {
  std::unique_ptr<A> pa(new A());
  std::unique_ptr<A> pb = std::move(pa);  // pb 에 소유권을 이전.
}

unique_ptr의 복사생성자는 삭제되어있지만 이동생성자는 허용되어 있으므로 이동생성자를 이용해 소유권 이전 가능

 

★ 하지만 소유권을 이전하고 난 후 위의 pa는 nullptr을 가리키고 있으므로 pa->some()같은걸 하면 런타임 오류가 발생함

그러므로 소유권 이전을 하려면 이전하기 전의 포인터는 다시는 참조하지 않는다는 확신이 있어야 함

 

 

 

함수의 인자로 unique_ptr을 전달하는 방법

 

레퍼런스로 전달하면 오류없이 잘 되지만 유일하게 소유권을 가진다는 개념을 위배하게 됨

그러므로 unique_ptr의 포인터 주소값을 전달하도록 해야 함

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

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

위처럼 unique_ptr 객체에 get함수를 사용하면 실제 객체의 주소값을 리턴해줌

 

 

 

unique_ptr을 쉽게 생성하기

 

C++ 14부터 unique_ptr을 쉽게 만들 수 있는 std::make_unique 함수를 제공함

int main() {
  auto ptr = std::make_unique<Foo>(3, 5);
  ptr->print();
}

std::unique_ptr<Foo> ptr(new Foo(3, 5)); 할걸 간단하게 위처럼 사용 가능

 

 

 

unique_ptr 사용시 주의사항

 

unique_ptr을 원소로 가지는 STL 컨테이너 사용시 unique_ptr은 복사생성자가 없으므로 주의해야 함

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

  vec.push_back(pa);  // ??
}

위처럼 사용하면 pa를 복사해서 vector에 넣어야 하는데 복사생성자가 delete 되어있으니 오류가 발생함

그러므로 pa를 벡터에 넣어주려면 move를 사용해야 함

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

  vec.push_back(std::move(pa));  // 잘 실행됨
}

 

 

 

강제로 할당 해제 하는 방법

 

만약 스코프가 끝나기 전에 원하는 시점에 delete처럼 할당을 해제 하고 싶다면 

ptr.reset(nullptr);

을 사용하면 된다.

 

 

 

총 정리하면

1) unique_ptr은 어떤 객체의 유일한 소유권을 나타내는 포인터이며, unique_ptr이 소멸될 때 가리키던 객체 역시 소멸된다.

2) 다른 함수에서 unique_ptr이 소유한 객체에 일시적으로 접근하고 싶다면 get()을 통해 해당 객체의 포인터를 전달한다.

3) 소유권을 이전하고 싶다면 move를 사용한다.

4) 컨테이너에 넣을땐 복사생성자가 사용 불가능한 점을 유의한다

5) 생성은 make_unique로 한다

6) 강제 할당 해제는 reset을 사용한다.

 

 

 

 

※ 참고 문헌

https://modoocode.com/229