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

Jangmanbo·2023년 3월 25일
0

Effective C++

목록 보기
13/33

서론

프로그래밍에서 자원은 사용을 마치고 나면 시스템에 돌려주어야 하는 모든 것을 말한다.
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문이 실행될 거란 보장이 없다. ...부분에서 어떤 일이 일어날지 모르기 때문이다.

객체가 해제되지 않는 경우

  1. 중간에 return이 존재
  2. createInvestmentdelete가 동일한 루프에 들어있으면서 continuegoto에 의해 루프를 빠져나옴
  3. delete 전에 어떤 코드에서 예외를 던짐

아무튼 객체를 해제하지 못하면 객체를 담고 있는 메모리가 누출되고, 그 객체가 갖고 있던 자원까지 샌다.


해결법: 자원을 객체에 넣어 자원 해제를 객체의 소멸자가 맡도록 하자!

대부분의 자원은 힙에서 동적으로 할당되고 어떤 블록이나 함수 안에서만 쓰이는 경우가 많다.
따라서 해당 블록이나 함수로부터 실행 제어가 빠져나올 때 자원이 해제되어야 한다.
C++은 자동으로 소멸자를 호출해주기 때문에 객체로 자원을 관리하면 해당 자원을 저절로 해제할 수 있다.

자원 관리에 객체를 사용하는 방법

1. 자원을 획득한 후에 자원 관리 객체에게 넘긴다.

자원이 그 자원을 관리할 객체의 초기화에 쓰인다.
이를 한마디로 RAII(Resource Acquisition is Initialization)라고 부른다.

2. 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.

소멸자는 객체가 소멸될 때 자동으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나든 자원의 해제가 이루어질 수 있다. (단, 객체를 해제하다가 발생하는 예외는 논외. 항목 8)

여기서 자원을 관리하는 객체로는 표준 라이브러리의 unique_ptrshared_ptr이 있다.


unique_ptr

  • 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 호출하는 스마트 포인터
  • 해당 객체에 대해 유일한 소유권을 가진다. (한 unique_ptr가 가리키는 객체를 다른 unique_ptr이 가리킬 수 없음)
  • 참고: 책에서는 auto_ptr로 설명하지만 auto_ptr은 C++11 부터 사용 중지 권고, C++17 부터 사용 불가능하다. 따라서 현재 이를 대체하고 있는 unique_ptr로 기술.

unique_ptr 사용 예시

void f()
{
	// 팩토리 함수 호출하여 자원을 획득하여 자원 관리 객체인 unique_ptr에 넘긴다.
	std::make_unique<Inventment> pInv(createInvestment());
    
    ...		// pInv 사용하기
}	// 2. unique_ptr의 소멸자를 통해 pInv 삭제

unique_ptr 특징: 어떤 객체를 가리키는 unique_ptr이 둘 이상이면 안된다.

unique_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 delete를 수행한다. 동일한 객체를 가리키는 두 unique_ptr 중 하나가 소멸된 후 나머지 하나가 다시 소멸한다면, 이미 삭제된 자원을 삭제하게 된다. (=> 미정의 동작)

따라서unique_ptr은 자신이 가리키는 객체에 대해 유일한 소유권을 가진다. 그러므로 복사 대입 연산자, 복사 생성자도 제공하지 않는다.
그렇다면 객체의 소유권을 이전하고 싶을 땐 어떻게 해야 할까.?

unique_ptr의 소유권 이전: move

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가 있다.

shared_ptr 사용 예시

void f()
{
	...
    std::tr1::shared_ptr<Investment> pInv(createInvestment());	// 팩토리 함수 호출
    ...	// pInv 사용
}	// shared_ptr의 소멸자를 통해 pInv 자동으로 삭제

unique_ptr과 사용법이 동일해보인다. 그러나 shared_ptrunique_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을 사용할 수 없는 상황에 적합하다.

unique_ptr, shared_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)객체를 사용하자!

0개의 댓글