- RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.
- RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 식으로 처리한다.
물론 다른 방법도 가능하다.
자원 관리 클래스의 복사 동작
힙에 생성되지 않는 자원은 스마트 포인터로 처리해 주기엔 일반적으로 맞지 않다.
예를 하나 들어보자. Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용 중이라고 가정해보자.
void lock(Mutex *pm); // pm이 가리키는 뮤텍스에 잠금을 건다.
void unlock(Mutex *pm) // pm이 가리키는 뮤텍스의 잠금을 푼다.
뮤텍스 잠금을 관리하는 클래스를 만드려고 한다. 뮤텍스 잠금을 잊지 않고 풀어 줄 목적이다.
RAII 기법을 이용하여 구현하려 한다.
class Lock {
public:
explicit Lock(Mutex *pm) : mutexPtr(pm) // 자원 획득
{ lock(mutexPtr); }
~Lock() { unlock(mutexPtr); } // 자원 해제
private:
Mutex *mutexPtr;
};
///////사용자/////////
Mutex m;
...
{
Lock m1(&m);
...
}
문제가 없어 보인다. 하지만, Lock 객체를 복사하는 상황에서 처리를 확실히 해줘야 한다.
Lock m11(&m); // 잠금
Lock m12(m11); // 어떻게 처리해야 할까?
몇 가지 선택지가 있다.
- 복사를 금지한다.
RAII 객체가 복사되도록 허용하는 것이 말이 안 되는 경우가 많다. 위의 예제도 그렇다.
어떤 스레드 동기화 객체에 대한 사본이라는 것이 의미가 없다. 복사하면 안 되는 RAII 클래스에 대해서는 반드시 복사가 되지 않도록 막아야 한다. 복사 함수를 private 멤버로 만드는 것이 좋은 방법이다.
class Lock: private Uncopyable {
public:
...
};
- 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 삭제하지 않는 게 바람직한 경우도 종종 있다.
이럴 경우에는, 해당 자원을 참조하는 객체의 개수에 대한 카운트를 증가시키는 식으로 RAII 객체의 복사 동작을 만들어야 한다. 이런 방식은 tr1::shared_ptr이 사용하고 있다.
그렇다면 tr1::shared_ptr을 클래스 멤버로 넣어 사용한다면 tr1::shared_ptr은 참조 카운트가 0이 될 때 자신이 가리키고 있던 대상을 삭제해 버리기 때문에 주의해야 한다.
tr1::shared_ptr이 삭제자지정을 허용하기 때문에 이를 활용하면 해결할 수 있다. 삭제자란 tr1::shared_ptr이 유지하는 참조 카운트가 0이 되었을 때 호출되는 함수 혹은 함수 객체를 말한다. 삭제자는 tr1::shared_ptr 생성자의 두 번째 매개변수로 선택적으로 넣어 줄 수 있다.
class Lock {
public:
explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) // shared_ptr 초기화, 삭제자 지정
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
Lock 클래스에 소멸자를 선언하지 않는다. 필요가 없기 때문이다. 컴파일러가 만드는 소멸자에서는 비정적 데이터 멤버의 소멸자를 자동으로 호출하게 되어 있어서 mutexPtr이 자동으로 소멸될 것이다. mutexPtr이 소멸되면서 지정한 삭제자가 호출되며 unlock을 진행할 것이다.
- 관리하고 있는 자원을 진짜로 복사한다.
때에 따라서 자원을 복사할 수도 있다. 이때는 자원을 다 썼을 때 각각의 사본을 확실히 해제한다.
자원 관리 객체를 복사하면 그 객체가 관리하는 자원까지 복사해야 한다. 즉, 깊은 복사를 수행해야 한다.
표준 string 타입을 살펴보면, 문자열을 구성하는 원소들을 힙 메모리에 저장해 놓고 이 메모리에 대한 포인터를 데이터 멤버로 갖고 있는 경우를 볼 수 있다. 이렇게 설계된 string 타입으로 생성한 객체는 결국 힙 메모리를 포인터로 물고 있는 형태이다. 이때 이 객체를 복사하면, 사본은 포인터 및 그 포인터가 가리키는 힙 메모리를 갖게 된다. - 관리하고 있는 자원의 소유권을 옮긴다.
어떤 특정한 자원에 대해 그 자원을 실제로 참조하는 RAII 객체는 딱 하나만 존재하도록 만들고 싶은 경우가 있다.
그 RAII 객체가 복사될 때는 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 한다. auto_ptr의 복사 동작이 이렇게 동작한다.
객체 복사 함수는 컴파일러에 의해 생성될 수 있기 때문에 원하는 방식과 다르다면 직접 구현해야 한다는 점을 주의해야 한다.