프로그래밍에서 자원은 사용을 마치고 나면 시스템에 돌려주어야 하는 모든 것을 말한다.
C++ 프로그램에서 자원은 대표적으로 동적 할당한 메모리가 있으며, 이 외에도 file descriptor, mutex lock, GUI 리소스, DB 연결, 네트워크 소켓 등등 모두 자원이다.
class Investment { ... };
다음은 여러 형태의 투자(주식, 채권, ...)를 모델링한 클래스들의 최상위 클래스 Investment
이다.
// Invenstment 파생 클래스의 객체를 동적 할당하여 그 포인터를 반환
Investment* createInvenstment(...);
Investment
에서 파생된 클래스들의 객체를 사용자가 얻어내기 위한 용도로 팩토리 함수를 선언한다. 항목 7
사용자는 팩토리 함수를 통해 얻은 객체를 다 사용하고 난 후 객체를 삭제해야 한다.
객체의 해제는 이 함수의 호출자(caller) 쪽에서 직접 해야한다.
void f()
{
Inventment *pInv = createInvestment(); // 팩토리 함수 호출
... // pInv 사용하기
delete pInv; // 객체 해제
}
언뜻 보면 팩토리 함수를 통해 얻은 객체를 사용하고 delete
로 해제하니 문제가 없어보인다. 그러나 이 함수의 delete
문이 실행될 거란 보장이 없다. ...
부분에서 어떤 일이 일어날지 모르기 때문이다.
return
이 존재createInvestment
와 delete
가 동일한 루프에 들어있으면서 continue
나 goto
에 의해 루프를 빠져나옴delete
전에 어떤 코드에서 예외를 던짐아무튼 객체를 해제하지 못하면 객체를 담고 있는 메모리가 누출되고, 그 객체가 갖고 있던 자원까지 샌다.
대부분의 자원은 힙에서 동적으로 할당되고 어떤 블록이나 함수 안에서만 쓰이는 경우가 많다.
따라서 해당 블록이나 함수로부터 실행 제어가 빠져나올 때 자원이 해제되어야 한다.
C++은 자동으로 소멸자를 호출해주기 때문에 객체로 자원을 관리하면 해당 자원을 저절로 해제할 수 있다.
자원이 그 자원을 관리할 객체의 초기화에 쓰인다.
이를 한마디로 RAII(Resource Acquisition is Initialization)라고 부른다.
소멸자는 객체가 소멸될 때 자동으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나든 자원의 해제가 이루어질 수 있다. (단, 객체를 해제하다가 발생하는 예외는 논외. 항목 8)
여기서 자원을 관리하는 객체로는 표준 라이브러리의 unique_ptr
과 shared_ptr
이 있다.
unique_ptr
- 가리키고 있는 대상에 대해 소멸자가 자동으로
delete
를 호출하는 스마트 포인터- 해당 객체에 대해 유일한 소유권을 가진다. (한
unique_ptr
가 가리키는 객체를 다른unique_ptr
이 가리킬 수 없음)- 참고: 책에서는
auto_ptr
로 설명하지만auto_ptr
은 C++11 부터 사용 중지 권고, C++17 부터 사용 불가능하다. 따라서 현재 이를 대체하고 있는unique_ptr
로 기술.
void f()
{
// 팩토리 함수 호출하여 자원을 획득하여 자원 관리 객체인 unique_ptr에 넘긴다.
std::make_unique<Inventment> pInv(createInvestment());
... // pInv 사용하기
} // 2. unique_ptr의 소멸자를 통해 pInv 삭제
unique_ptr
은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 delete
를 수행한다. 동일한 객체를 가리키는 두 unique_ptr 중 하나가 소멸된 후 나머지 하나가 다시 소멸한다면, 이미 삭제된 자원을 삭제하게 된다. (=> 미정의 동작)
따라서unique_ptr
은 자신이 가리키는 객체에 대해 유일한 소유권을 가진다. 그러므로 복사 대입 연산자, 복사 생성자도 제공하지 않는다.
그렇다면 객체의 소유권을 이전하고 싶을 땐 어떻게 해야 할까.?
std::unique_ptr<Investment> pInv1(createInvestment());
std::unique_ptr<Investment> pInv2 = pInv1; // 복사 대입 연산자 시도. 컴파일 에러
std::unique_ptr<Investment> pInv3 = std::move(p1); // 소유권 이전
RCSP(reference-counting smart pointer)
- 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터
- 앞서 언급한 `unique_ptr'와 달리 여러 객체가 소유권을 가지고 싶을 경우 RCSP가 대안이 될 수 있다.
- 대표적으로
shared_ptr
가 있다.
void f()
{
...
std::tr1::shared_ptr<Investment> pInv(createInvestment()); // 팩토리 함수 호출
... // pInv 사용
} // shared_ptr의 소멸자를 통해 pInv 자동으로 삭제
unique_ptr
과 사용법이 동일해보인다. 그러나 shared_ptr
은 unique_ptr
과 달리 복사가 가능하다.
void f()
{
std::shared_ptr<Investment> pInv1(createInvestment());
std::shared_ptr<Investment> pInv2(pInv1); // pInv1, pInv2가 동시에 객체를 가리킨다.
pInv1 = pInv2; // 마찬가지로 pInv1, pInv2가 동시에 객체를 가리킨다. (변함 없음)
} // pInv1, pInv2가 소멸되며 이들이 가리키고 있는 객체도 자동으로 삭제
복사동작이 대부분의 사용자가 원하던 대로 이루어지기 때문에 unique_ptr
을 사용할 수 없는 상황에 적합하다.
std::unique_ptr<std::string> aps(new std::string[10]); // 잘못된 코드
std::shared_ptr<int> spi(new int[1024]); // 마찬가지로 잘못된 코드
소멸자 내부에서 delete
연산자를 사용한다. (delete []
가 아니라! 항목 16) 따라서 동적 할당한 배열에 unique_ptr
이나 shared_ptr
을 사용하면 안된다.
결론: 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 자원을 해제하는 RAII(ex. shared_ptr
, unique_ptr
)객체를 사용하자!