본문 바로가기
Object Oriented Programming(C++)/Effective C++

Effective C++ | 항목 13 자원은 객체로 관리하자

by continue96 2022. 1. 23.

항목 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가 복사 동작이 직관적이기 때문에 대개 더 좋다.

 

 

댓글