리소스의 종류에 상관없이, 리소스는 사용이 끝나면 항상 release해야 한다.
Item 13 : Use objects to manage resources.
자원 관리에는 객체가 그만.
아래 코드를 살펴보자.
Investment* createInvestment(); //return ptr to dynamically allocated
void f()
{
Investment *pInv = createInvestment();
...
delete pInv;
}
이 코드는 괜찮아 보이지만, 함수 f는 createInvestment 함수를 통해 얻은 오브젝트를 delete하는데 실패할 수 있다.
함수의 중간에 return문이 위치하면 delete가 실행되지 않고, 메모리 누수 문제가 발생한다.
따라서, 함수 f가 delete 구문에 의존하는 것은 좋지 않은 방법이다.
createInvestment를 통해 리턴된 리소스가 항상 released되도록 하기 위해, 리소스를 오브젝트에 포함할 수 있다.
그리고 오브젝트의 소멸자를 통해 자동으로 리소스가 release되도록 한다.
그러나, 이 방법은 C++의 자동 소멸자 호출에 의존한다.
많은 리소스들은 dynamically하게 heap에 할당되고 그 block또는 function을 벗어나면 release되어야 한다.
Standard library의 auto_ptr을 자원관리에 사용할 수 있다.
auto_ptr은 smart pointer로 자동으로 delete를 호출하기 때문에 메모리 누수 문제를 방지할 수 있다.
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
}
이 간단한 코드는 자원관리에 오브젝트를 사용하는 두가지 중요한 측면을 드러낸다.
1. 리소스는 얻어짐과 동시에 resource-managing object가 되어야 한다.
리소스 관리에 오브젝트를 사용하는 것을 Resource Acquisition Is Initialization(RAII)라고 한다.
리소스를 얻고 동시에 resource-managing object로 초기화 하는 것은 흔하다.
2. Resource-managing object들은 리소스가 release된다는 것을 보장하기 위해 소멸자를 사용해야 한다.
오브젝트가 없어질 때 소멸자가 자동으로 호출되기 때문에 리소스는 알맞게 release 될 수 있다.
auto_ptr은 삭제될 때 자동으로 delete한다.
한 오브젝트를 가리키는 auto_ptr이 한 개만 존재해야 한다.
만약 그렇지 않으면, 의도하지 않은 동작이 발생할 수 있다.
이러한 문제를 방지하기 위해 auto_ptr은 copy constructor 또는 copy assignment operator를 통해 복사하면 null로 설정되고 copying pointer가 리소스의 단독 소유권을 가지도록 한다.
그러나 이 기본 요구 사항은 auto_ptr이 동적으로 할당된 리소스를 관리하는 좋은 방법이 아님을 의미한다.
auto_ptr의 대안으로 reference-counting smart pointer(RCSP)가 있다.
RCSP는 한 오브젝트를 가리키는 포인터의 수를 추적하고 아무도 그 오브젝트를 가리키지 않으면 자동으로 delete하는 스마트 포인터다.
garbage collection과 비슷하지만 cycle of reference를 break하지 못한다.
auto_ptr과 tr1::shared_ptr은 destructor에 delete[]가 아닌 delete를 사용한다는 점에 주의해야 한다.
즉, auto_ptr과 tr1::shared_ptr을 동적으로 할당되는 array에 사용하는 것은 나쁜 생각이다.
이 경우에는 vector와 string을 사용할 수 있다.
정리하면, 메모리 누수를 방지하기 위해서 생성자에서 리소스를 얻고 소멸자에서 release 하는데에 RAII 오브젝트를 사용해야 한다.
두 RAII 클래스, TR1::shared_ptr과 auto_ptr이 유용하지만, shared_ptr이 더 좋은 선택이다.
Item 14 : Think carefully about copying behavior in resource-managing classes.
자원 관리 클래스의 복사 동작에 대해 진지하게 고민하자.
Item13에서 자원 관리 객체인 RAII를 사용하라고 언급했다.
그러나, 모든 리소스는 heap-based가 아니며 어떤 리소스들은 관리에 스마트 포인터가 부적절할 수 있다.
이러한 경우 직접 자원 관리 클래스를 만들어야 한다.
예를들어, Mutex 오브젝트를 관리하는 클래스가 있다고 가정하자.
class Lock{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{ Lock(mutexPtr); }
~Lock() { unlock(mutexPtr); }
private:
Mutex* mutexPtr;
};
Lock 오브젝트가 복사된다면 어떤 일이 발생하는가?
Mutex m;
Lock m11(&m);
Lock m12(m11);
RAII 오브젝트가 복사 될 때 선택할 수 있는 동작들은 다음과 같다.
1. 복사 금지
많은 경우에, RAII 오브젝트를 복사하는 것은 의미가 없다.
이것은 Lock 클래스도 마찬가지다.
synchronization primitive의 복사본을 갖는 것은 의미가 없기 때문이다.
복사를 금지할 때, 복사 생성자를 private으로 만들어야 한다(item 6 참고).
2. 관리하는 자원의 Reference-count 수행하기
자원을 사용중인 마지막 오브젝트가 소멸될 때 까지 자원을 유지해야 하는 경우 shared_ptr을 사용할 수 있다.
종종, RAII 클래스들은 reference-counting 복사를 shared_ptr을 멤버 데이터로 포함시킴으로써 구현할 수 있다.
예를 들어, 만약 Lock 클래스가 referece-counting을 원하는 경우, mutexPtr을 Mutex*에서 TR1::shared_ptr<Mutex>로 바꿀 수 있다.
하지만, shared_ptr은 참조 개수가 0이 되면 리소스를 해제하기 때문에 Mutex에는 맞지 않다.
Mutex의 경우에는 모두 사용이 끝났을 때 리소스 해제가 아닌 unlock을 해야 한다.
shared_ptr은 deleter를 특정할 수 있는데, 이를 활용하면 참조 개수가 0이 될때 unlock이 되도록 할 수 있다.
deleter는 optional한 두 번째 파라미터로 shared_ptr의 생성자에 전달할 수 있다.
class Lock{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm, unlock) // deleter로 unclock 지정
{ Lock(mutexPtr.get()); }
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
위 코드의 경우, 참조 개수가 0이 되면, unlock이 호출된다.
3. 관리하는 자원 복사
자원 관리 클래스의 복사가 필요한 경우 deep copy를 사용한다.
string은 deep copy를 지원한다.
4. 관리하는 자원의 소유권 이전
오직 하나의 RAII 오브젝트만 자원을 참조하도록 하고 싶은 경우, 복사한 오브젝트로 소유권을 이전할 수 있다.
이는 auto_ptr을 사용한 복사와 같다.
정리하면, RAII 오브젝트를 복사하는 것은 그것이 관리하는 리소스를 복사하는 것도 포함한다.
따라서, 리소스를 복사하는 방법은 RAII 오브젝트의 복사 방법에 따라 결정해야 한다.
RAII 클래스의 복사 동작은 흔히 복사를 금지하고 reference counting을 수행한다.
하지만 다른 동작들은 가능하다.
Item 15 : Provide access to raw resources in resource-managing classes.
자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자.
많은 API들은 자원을 직접적으로 참조할 수 있도록 한다.
따라서 API를 사용할 때, 때때로 자원 관리 객체를 건너 뛰고 자원을 직접 다룰 수 있도록 해야 한다.
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment *pi); // return number of days
int days = daysHeld(pInv) // error!!!
위의 코드는 오류가 발생한다.
daysHeld함수는 raw Investment* pointer를 원하는데, shared_ptr<Investment>타입의 객체를 넘겨줬기 때문이다.
따라서, RAII 클래스의 객체를 raw resource로 바꿀 수 있는 방법이 필요하다.
1. explicit conversion
shared_ptr과 auto_ptr은 get member function을 제공한다.
즉, 스마트 포인터 객체 안의 raw pointer를 반환한다.
int days = daysHeld(pInv.get()); // fine, passes the raw pointer
2. implicit conversion
사실상 다른 모든 스마트 포인터처럼, shared_ptr과 auto_ptr은 dereferencing operator(operator->, operator*)를 오버로드 한다.
그러나, implicit conversion은 에러 발생 가능성을 높인다.
explicit, implicit conversion을 선택할 때는 RAII 클래스가 수행해야 하는 작업, 클래스가 어떤 의도로 사용되도록 만들어 졌는지를 고려해야 한다.
가장 좋은 방법은 인터페이스를 정확히 쉽게 사용할 수 있도록 만드는 것이다.
일반적으로 explicit conversion이 안전하기 때문에 많이 사용하나, implicit conversion이 더 클라이언트가 사용하기 편리하다는 장점이 있다.
Item 16 : Use the same from in corresponding uses of new and delete.
new 및 delete를 사용할 때는 형태를 반드시 맞추자.
new 표현식을 사용하면 메모리가 할당되고, 한 개 이상의 생성자가 그 메모리에 호출된다.
그리고 delete 표현식을 사용하면, 메모리에 대한 한 개 이상의 소멸자가 호출되고 메모리가 할당 해제된다.
얼마나 많은 삭제되어야 하는 오브젝트가 메모리에 위치하는가?
삭제되어야 하는 오브젝트의 개수에 따라 소멸자가 호출되어야 한다.
이 문제는 간단하다.
포인터가 하나의 객체를 가리키는가 아니면 객체의 array를 가리키는가?
하나의 객체에 대한 레이아웃과 객체의 array에 대한 레이아웃은 다르다.
array는 보통 array의 size를 포함한다.
물론, 이것은 단지 예시다. 컴파일러는 이런 방식의 구현을 필요로 하지 않는다.
포인터에서 delete를 하는 경우, array size information이 있는지 여부를 알 수 있는 유일한 방법은 사용자가 알려주는 것이다.
delete를 사용할 때 대괄호[]를 사용하는 경우, delete는 배열을 가리킨다고 가정한다.
대괄호를 사용하지 않으면, 단일 객체를 가리킨다고 가정한다.
std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
delete stringPtr1;
delete[] stringPtr2;
만약, new 표현식에서 []를 사용한다면, delete 표현식에서도 []를 사용해야 한다.
그리고 만약 new 표현식에서 []를 사용하지 않는다면, delete 표현식에서도 []를 사용하지 않는다.
Item 17 : Store newed objects in smart pointers in standalone statements.
new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자.
다음 함수를 사용하는 코드들을 살펴보자.
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
processWidget(new Widget, priority());
위 코드는 컴파일 되지 않는다. shared_ptr의 생성자는 explict로 선언되어 있다.
그러나 new Widget 표현식을 통해 리턴되는 raw pointer는 shared_ptr 타입으로 implict conversion이 되지 않는다.
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
이 방법은 컴파일 되지만, 메모리 누수가 발생할 가능성이 있다.
processWidget이 호출되기 전에, 컴파일러는 3가지 코드를 생성한다.
priority 호출 → new Widget 실행 → tr1::shared_ptr 생성자 호출
new Widget 표현식은 shared_ptr의 생성자가 호출되기 전에 반드시 실행되어야 한다.
하지만 priority 호출은 어떤 순서에 위치하던지 상관이 없다.
만약 컴파일러가 priority 호출을 두 번째로 지정하면, 다음과 같은 순서로 실행된다.
new Widget 실행 → priority 호출 → tr1::shared_ptr 생성자 호출
이 상황에서 priority가 예외가 발생하는 경우, 문제가 발생한다.
이 경우 new Widget 실행을 통해 반환되는 포인터를 잃어 버릴 수 있고 이로인해 메모리 누수가 발생한다.
이 문제를 피하기 위한 방법은 간단하다.
Widget을 생성하고 그것을 스마트 포인터에 저장하는 구문과 스마트 포인터를 processWidget에 전달하는 구문을 따로 작성한다.
std::tr1::shared_ptr<Widget>pw(new Widget);
processWidget(pw, priority());
이것은 잘 작동한다.
priority 호출과 shared_ptr 생성자 호출이 다른 구문에 위치하기 때문에 priority 호출이 Widget 생성과 shared_ptr 생성자 호출 사이에 위치할 수 없다.