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을 사용한다.
※ 참고 문헌