[Effective C++] 13. 자원 관리에는 객체가 그만!

C++/Effective C++

2020. 8. 3. 20:39

1. 호출자가 관리하는 자원의 문제점

void f(){
    Investment* pInv = new Investment();
    ...
    
    delete pInv;
}

pInv가 delete 되기 전에 여러가지 이유에 의해 (goto,return,예외 등등..) 자원을 반납하지 못한 체 함수 밖으로 나갈수 있다. delete 문을 건너뛰는 경우 객체의 메모리가 누출되고, 그와 동시에 그 객체가 갖고 있던 자원까지 모두 센다.

 

2. 자원을 항상 해제시키는 방법

자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 f를 떠날 때 호출되도록 만드는 것이다. 자원을 객체에 넣음으로써, C++가 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해체할 수 있다.

 

3. 스마트 포인터

위의 방식으로 자원을 관리하는 방법으로 C++이 제공해 주는 스마트 포인터를 사용할 수 있다. 스마트 포인터는 자원 획득 즉 초기화(Resource Acquisition Is Initialization) 방식으로 설계되어 해당 리소스 사용 scope가 끝날 경우에 자동으로 해제해주며 exception이 발생하는 경우에도 메모리 해제를 보장해 준다. 다음으로,스마트 포인터 사용의 두 가지 특성을 알아 보자.

 

void f(){
    std::unique_ptr<Investment> pInv(createInvestment());
    
    ...
}

 

  • 첫째, 자원을 획득한 후에 자원 관리 객체(스마트 포인터)에게 넘깁니다.
  • 둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 합니다.

그러나, unique ptr은 오직 하나의 객체만이 소유권을 가질수 있으며 copy가 일어나면 소유권이 copy를 사용한 객체 쪽으로 이동하게 된다. 따라서, 이렇게 비정상적인 복사과정에 의해 stl 컨테이너를 사용할 수 없게 된다.

 

std::unique_ptr<Investment>
pInv(createInvestment());
std::unique_ptr<Investment> pInv2(pInv1) // pInv2로 소유권 이동, pInv1 = nullptr가 된다.
pInv1 = pInv2; // pInv1로 소유권 이동, pInv2 = nullptr가 된다.

이런 경우 참조 카운팅 방식 스마트 포인터를 사용할 수 있다.

 

void f(){

	...
	std::shared_ptr<Investment> pInv(createInvestment());
	std::shared_ptr<Investment> pInv2(pInv); // pInv,pInv2가 동시에 객체를 가리킴
    pInv = pInv2; // 마찬가지
    
}
// scope를 벗어나면 자동으로 메모리 해제

이 경우 STL 컨테이너 등의 환경에 딱 맞게 쓸 수 있다. 

 

책의 경우에는 배열과 관련해서 스마트 포인터를 사용하는 것이 난감하다고 나와있다. 하지만 C++ 11 이후에 완전히 문법화 된 스마트 포인터의 경우 Deleter를 통해 배열인 경우에 대해서 메모리를 정확히 해제할 수 있다.

 

#include <functional>
#include <memory>

template<typename T>
std::function<void (T*)> array_deleter() {
    return [](T* ptr) { delete[] ptr; };
}

int main()
{
    std::shared_ptr<int> foo(new int[1024], array_deleter<int>());
    
    foo.reset();
    
    return 0;
}
// 출처 : https://blog.koriel.kr/cpp11-smart-pointer/

 

이것만은 잊지 말자!

  • 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용합시다.
  • 일반적으로 널리 쓰이는 RAII 클래스는 unique_ptr, shared_ptr이다. 이 둘 가운에 shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋습니다. 반면, unique_ptr은 복사되는 객체를 null로 만들어 버립니다.