자원 관리를 못하는 순간
class A {
int *data;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl;
}
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
void thrower() {
// 예외를 발생시킴!
throw 1;
}
void do_something() {
A *pa = new A();
thrower();
// 발생된 예외로 인해 delete pa 가 호출되지 않는다!
delete pa;
}
int main() {
try {
do_something();
} catch (int i) {
std::cout << "예외 발생!" << std::endl;
// 예외 처리
}
}
- 잘못된 메모리 관리로 인한 문제점
- 예외는 정상적으로 처리되었으나, 소멸자가 호출되지 않아 메모리 누수가 발생
- 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상
- 사용 후 반환되지 않는 메모리가 누적되면 메모리가 낭비
- 이미 해제된 메모리를 다시 참조하는 경우 (double free)
// 같은 메모리 주소를 2개의 포인터가 공유
// 객체의 소유권이 명확하지 않은 경우
Data* data = new Data();
Date* data2 = data;
// data 의 입장 : 사용 다 했으니 소멸시켜야지.
delete data;
// ...
// data2 의 입장 : 나도 사용 다 했으니 소멸시켜야지
delete data2;
RAII
- Managed Language 는 가비지 컬렉터가 기본적으로 내장
- 프로그램 상에서 더 이상 쓰이지 않는 자원을 자동으로 해제
- 프로그래머들이 자원을 해제하는 일로부터 자유로움
- Unmanaged Language인 C++는 다름
- Resource Acquisition Is Initialization
- 자원 관리는 초기화이다
- 자원 관리를 스택에 할당한 객체를 통해 수행
- stack unwinding을 통해 예외가 발생하더라도, stack frame을 빠져나갈 때 소멸자를 호출하는 원리를 이용
- 스택의 항상성 유지를 위함
- 예외 발생시, 함수 내부에 catch 구문이 없다면 호출원을 거슬러 올라가여 스택을 정리
- 일반적인 포인터가 아닌, 객체로서의 포인터를 정의
- auto_ptr을 보완하는 새로운 스마트 포인터
raw pointer over than smart pointer
- 소유권 개념이 없는 모든 경우
- legacy 코드를 고려해야 하는 경우
unique_ptr
- 객체에 대한 유일한 소유권을 보장
- Memory leak과 double free를 방지
- std::move를 이용해 소유권 이전은 가능
- 이동 전의 dangling pointer를 다시 사용하지 않는다는 확신하에 사용
// unique_ptr 을 사용한 포인터 할당
std::unique_ptr<A> pa(new A);
// 위 구문은 아래와 동일
A* pa = new A();
// 아래의 구문은 컴파일 에러를 발생
// unique_ptr는 복사 생성자를 명시적으로 삭제
// unique_ptr는 어떤 객체를 유일하게 소유하고 있어야 하기 때문
// -> 삭제된 함수를 사용하려는 시도
std::unique_ptr<A> pb = pa;
// 소유권 이전은 가능
// pa의 주솟값은 0 (NULL)로 변환
std::unique_ptr<A> pc = std::move(pa);
- 함수 인자로 전달할 때는 주솟값을 그대로 전달
- 함수 인자 타입을 unique_prt&로 전달하는 방법?
// 올바르지 않은 전달 방식:
// unique_ptr& 인자로 받으면서
// 유일한 소유권의 의미가 함수 인자 ptr을 통해서도 행사가 가능해짐
// => 단순한 Wrapper
void do_something(std::unique_ptr<A>& ptr)
{
ptr->do_sth(3);
}
// 올바른 전달 방식:
// 실제 객체의 주솟값을 전달
// 함수 내부에서 객체의 주소를 통해 직접 접근할 수 있는 권한 획득
void do_something(A* ptr)
{
ptr->do_sth(3);
}
int main() {
std::unique_ptr<A> pa(new A());
// 올바르지 않은 전달 방식:
do_something(pa);
// 올바른 전달 방식:
// get 함수는 실제 객체의 주소값을 전달
// 소유권이라는 의미를 떠나,
// do_something 함수 내부에서 객체에 접근할 수 있는 권한을 획득
do_something(pa.get());
}
C++14부터 make_unique 함수 지원
class Foo {
int a, b;
public:
Foo(int a, int b) : a(a), b(b) { std::cout << "생성자 호출!" << std::endl; }
void print() { std::cout << "a : " << a << ", b : " << b << std::endl; }
~Foo() { std::cout << "소멸자 호출!" << std::endl; }
};
int main() {
// 이 구문은 아래의 구문과 동일
std::unique_ptr<Foo> ptr(new Foo(3, 5));
// 이 구문은 위 구문과 동일
auto ptr = std::make_unique<Foo>(3, 5);
ptr->print();
}
unique_ptr의 컨테이너
int main() {
std::vector<std::unique_ptr<A>> vec;
std::unique_ptr<A> pa(new A());
// case 1
// 컴파일 오류:
// 삭제된 복사 생성자에 접근하기 때문
vec.push_back(pa);
// case 2
// 정상적인 컴파일:
// push_back이 우측값 레퍼런스를 받는 버전으로 오버로딩
vec.push_back(std::move(pa));
// case 3
// 정상적인 컴파일:
// vec.push_back(std::unique_ptr<A>(new A())); 과 동일
// perfect forwarding
// 불필요한 이동 연산 없음
vec.emplace_back(new A());
}
- 위와 같은 경우는 컨테이너의 인덱싱을 통한 접근으로만 사용할 목적이어야 한다
- unique_ptr을 사용한 객체에 대한 소유권은 단 하나이기 때문에, 컨테이너에 넣기 전 외부에서 생성한 pa로는 더 이상 접근할 수 없다
- 만약 컨테이너 인덱싱으로도, 외부에서 생성한 객체로도 모두 주소값에 접근하고 싶다면 unique_ptr을 사용하면 안된다
uniqure_ptr 을 리턴하는 함수
- uniqure_ptr은 복사 혹은 대입이 불가능
std::unique_ptr<int> func()
{
std::unique_ptr<int> ptr(new int [10]);
return ptr;
}
- 그러나 위와 같은 함수를 만들 수 있다
- 함수가 반환하는 값은 rvalue이기 때문
- 따라서 위의 함수는 실제로 아래 함수처럼 작동한다
std::unique_ptr<int> func()
{
std::unique_ptr<int> ptr(new int [10]);
return std::move(ptr); // 이동생성자 unique_ptr(unique_ptr&&) 호출
}
- 그러나 위의 방법은 실제 반환하는 값의 타입과 함수의 리턴 타입이 다른 경우에 사용하는 걸 권장한다
- move를 사용하지 않으면 컴파일러가 move contructor를 호출하지 않고 RVO 최적화를 실행할 수 있다.
std::unique_ptr<Base> func()
{
std::unique_ptr<Derived> ptr(new int [10]);
return std::move(ptr);
}
멤버 변수로서의 unique_ptr
class A {
private:
std::unique_ptr<MyType> mt;
public:
void initStuff() {
mt = StaticFuncSomewhereElese::generateMyType();
}
};
std::unique_ptr<MyType> StaticFuncSomewhereElese::generateMyType() {
auto temp = std::make_unique<MyType>(…);
// `make_unique` is C++14 (although trivially implementable in C++11).
// Here's an alternative without `make_unique`:
// std::unique_ptr<MyType> temp(new MyType(…));
//do stuff to temp (read file or something...)
return temp;
}
- 멤버 변수 unique_ptr은 최대한 빠르게 할당해주는 것이 좋다
class Square
{
public:
...
// 소유권을 가져오지 않기 위해 raw pointer로 반환
const Piece* getPiece() const;
std::unique_ptr<Piece> setPiece(std::unique_ptr<Piece> piece);
...
protected:
std::unique_ptr<Piece> piece;
...
};
const Piece* Square::getPiece() const
{
return piece.get();
}
std::unique_ptr<Piece> Square::setPiece(std::unique_ptr<Piece> piece)
{
// 기존에 가지고 있던 포인터를 old에 할당
std::unique_ptr<Piece> old = std::move(this->piece);
// 받은 인자를 move를 통해 멤버 변수로 이동
this->piece = std::move(piece);
// 사용하던 포인터를 반환 (삭제 혹은 사용하지 않도록 하기 위함)
return std::move(old);
}
출처