Item 14: 자원 관리 클래스를 복사할 때 주의하자
1. RAII를 이용하여 Mutex를 관리하는 간단한 예를 들어보자.
void lock(Mutex *pm); // 뮤텍스에 대한 권한을 얻어온다.
void unlock(Mutex *pm); // 뮤텍스에대한 권한을 반환한다.
// RAII class
class Lock {
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr); // 뮤텍스에 대한 권한을 얻어온다
}
~Lock(Mutex *pm)
{
unlock(mutexPtr); // 뮤텍스에 대한 권한을 반환한다
}
private:
Mutex *mutexPtr;
};
사용할 때는 아래처럼 사용한다.
Mutex m; // 사용할 뮤텍스를 정의한다
...
{ // 임계영역을 정하기 위해 블록을 만든다
Lock(&m); // 뮤텍스에 잠금을 설정한다
... // 임계영역에서 작업을 한다
} // 블록의 끝. 뮤텍스에 걸렸던 잠금이 자동으로
// 해제된다.
2. 만약 lock 객체가 아래처럼 복사된다면 어떻게 해야 하나?
Lock ml1(&m); // m에 잠금을 건다
Lock ml2(ml1); // ml1을 ml2에 복사한다.
// 어떻게 동작해야 하나?
경우에 따라 다른 동작을 해야 한다.
- 복사를 금지한다
실제로 RAII 객체를 복사하면 안되는 경우가 가장 많다.
이를 막기 위해서 복사함수들을 private으로 만들면 된다. Item 6 참조.
class Lock: public Uncopyable { // 복사를 금지한다.
public: // 항목 6을 참조하자.
... // 나머지는 이전과 같다.
};
- 관리하고 있는 객체에 대한 참조카운팅을 수행한다.
자원을 사용하고 있는 마지막 객체가 소멸될 때까지 자원을 반환하지 않아야 하는 경우에 사용하자.
대표적인 예가 shared_ptr이다.
shared_ptr은 참조 카운트가 0이 될때 자신이 가리키고 있는 대상을 버리도록 되어 있다.
위의 예에서는 Mutex를 지우는 것이 아니라 락만 해제해야 한다. 다행히 shared_ptr은 사용자 정의 삭제자를 지원한다. 아래 처럼 사용이 가능하다.
class Lock {
public:
explicit Lock(Mutex *pm) // shared_ptr을 초기화하는데, 가리킬 포인터로
: mutexPtr(pm, unlock) // Mutex 객체의 포인터를 사용하고 삭제자로
{ // unlock 함수를 지정한다.
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
Lock 클래스에 소멸자가 없음에 유의하라.
- 컴파일러가 자동으로 만드는 소멸자는 비정적 데이터 멤버에 대한 소멸자를 자동으로 호출해준다. 여기에서는 mutexPtr이 해당된다.
- mutexPtr의 소멸자는 뮤텍스의 참조 카운트가 0이 될 때 삭제자로 지정된 unlock 함수를 호출해 준다.
+++
- 관리하고 있는 자원을 정말로 복사한다.
Deep copy를 해야 하는 경우가 이것이다.
대표적인 경우가 string 클래스이다. string을 다른 string에 대입하면 힙에 똑같은 문자스트링이 하나 더 만들어 지게 된다. 그리고 각string 객체는 서로 다른 메모리 영역을 갖게된다.
- 관리하고 있는 자원의 소유권을 넘긴다.
흔한 경우는 아니다. 특정한 자원에 대해 그 자원을 실제로 참조하는 RAII 객체를 오직 하나만 존재하게 하고싶을 때 사용한다. 대표적인 예가 auto_ptr 이다.
정리
- RAII 객체의복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.
- RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리하는 것이다. 하지만 이 외의 방법들도 가능하니 참고해 두자.