항목 13 자원은 객체로 관리하자
13.1 자원 관리 클래스
투자를 모델링해주는 클래스 라이브러리에 Investment라는 최상위 클래스가 있고 이것을 기본으로 다양한 투자 클래스가 파생되어 있다고 가정해보자. 이 라이브러리는 Investment에서 파생된 클래스의 객체를 사용자가 얻어내는 용도로 팩토리 함수(factory function)를 쓰도록 만들어져 있다.
class Investment { ... }; /* 여러 형태의 투자를 모델링한 최상위 클래스 Investment */
Investment* createInvestment(); /* Investment 클래스 계통의 객체를 동적 할당하고 그 포인터를 반환하는 팩토리 함수 */
createInvestment 함수를 통해 얻은 객체를 사용할 일이 없을 때, 이 함수의 호출자(caller) foo는 그 객체를 삭제해야 한다. 그러나 createInvestment 함수로부터 얻은 객체를 삭제할 수 없는 경우가 많다.
void foo() {
Investment* pInv = createInvestment(); /* 팩토리 함수를 호출합니다. */
... /* 여기서 도중하자 return문, 익셉션 등으로 delete문까지 도달하지 못할 수 있습니다. */
delete pInv; /* 객체를 해제합니다. */
}
그러므로 createInvestment 함수로 얻은 자원이 항상 해제되도록 하는 방법은 자원을 객체에 넣고 자원 해제를 소멸자가 맡아 실행 제어가 foo를 떠날 때 소멸자가 호출되도록 하는 것이다. 다시 말해 자원을 객체에 넣음으로써 C++가 자동으로 호출해주는 소멸자에 의해 그 자원을 저절로 해제할 수 있다.
13.2 스마트 포인터
자원 관리 객체를 사용하는 데 중요한 두 가지 특징은 다음과 같다.
- 첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다. 자원을 획득하고 나면 바로 자원 관리 객체에게 넘겨준다. 이렇게 자원을 관리하는 데 객체를 사용하는 아이디어를 자원 획득 즉 초기화(resource acquisition is initialization, RAII)라 한다.
- 둘째, 자원 관리 객체는 소멸자를 사용해서 자원이 해제되도록 한다. 소멸자는 어떤 객체가 소멸될 때 자동으로 호출되기 때문에 실행 제어가 블록 혹은 함수로부터 빠져나올 때 자원 해제가 이루어진다.
■ 13.2.1 auto_ptr
표준 라이브러리를 보면 auto_ptr는 포인터와 비슷하게 동작하는 객체, 즉 스마트 포인터(smart pointer)로서 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어 있다. 이 예제를 보면 createInvestment 함수가 만들어준 자원을 그 자원을 관리할 auto_ptr 객체를 초기화하는 데 쓰이고 있다.
void foo {
std::auto_ptr<Investment> pInv(createInvestment());
... /* pInv를 마음껏 사용합니다. */
}
auto_ptr는 자신이 소멸될 때 가리키고 있는 대상에 대해 자동으로 delete를 불러주기 때문에, 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 자원이 두 번 삭제되는 결과를 낳게 된다. 이것을 막기 위해 auto_ptr는 복사 생성자 혹은 복사 대입 연산자로 객체를 복사하면 원본 객체를 null로 만들어 버린다.
std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1); /* pInv2가 객체를 가리키고, pInv1은 null입니다. */
pInv2 = pInv2; /* 다시 pInv1이 객체를 가리키고, pInv2는 null입니다.*/
CAUTION C++17부터 auto_ptr는 표준 라이브러리에서 삭제되었다.
■ 13.2.2 shared_ptr
auto_ptr를 쓸 수 없는 상황이라면 참조 카운팅 방식 스마트 포인터(reference counting smart pointer, RCSP)인 shared_ptr가 좋다. RCSP는 어떤 특정한 자원을 가리키는 객체의 개수를 보유하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터다.
void foo {
std::shared_ptr<Investment> pInv(createInvestment());
... /* pInv를 마음껏 사용합니다. */
}
shared_ptr는 auto_ptr에 비해 복사 동작이 정상적으로 이루어지기 때문에 이상한 복사 동작으로 인해 auto_ptr를 쓸 수 없는 STL 컨테이너 등의 환경에 딱 알맞게 쓸 수 있다.
void foo {
std::shared_ptr<Investment> pInv1(createInvestment());
std::shared_ptr<Investment> pInv2(pInv1); /* pInv1과 pInv2가 같은 객체를 가리킵니다. */
pInv1 = pInv2; /* 역시 pInv1과 pInv2는 같은 객체를 가리킵니다. */
}
■ 13.2.3 동적 할당한 배열
auto_ptr 및 shared_ptr는 소멸자 내부에서 delete[] 연산자가 아닌 delete 연산자를 사용한다. 다시 말해, 동적으로 할당한 배열에 대해 auto_ptr나 shared_ptr를 사용하면 안 된다. 동적으로 할당한 배열은 이제 vector나 string으로 거의 대체할 수 있다.
std::auto_ptr<std::string> aps(new std::string[10]); /* 잘못된 delete가 사용됩니다. */
std::shared_ptr<int> spi(new int[10]); /* 역시 잘못된 delete가 사용됩니다. */
NOTE
① 자원 누출을 막기 위해 생성자 안에서 자원을 획득하고 소멸자에서 자원을 해제하는 RAII 객체를 사용하자.
② 일반적으로 널리 쓰이는 RAII 클래스는 shared_ptr와 unique_ptr이다. shared_ptr가 복사 동작이 직관적이기 때문에 대개 더 좋다.
'Object Oriented Programming(C++) > Effective C++' 카테고리의 다른 글
Effective C++ | 항목 15 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2022.01.25 |
---|---|
Effective C++ | 항목 14 자원 관리 클래스의 복사 동작을 고찰하자 (0) | 2022.01.24 |
Effective C++ | 항목 12 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2022.01.12 |
Effective C++ | 항목 11 operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2022.01.12 |
Effective C++ | 항목 10 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2022.01.11 |
댓글