항목 14 : 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
자원 관리 클래스를 스스로 만들어야 할 필요를 느끼는 경우가 있습니다.
예를 하나 들어 보죠, Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용하는 중이라고 가정합시다.
이 C API에서 제공하는 함수 중엔 lock 및 unlock이 있고요.
1
2
3
|
void lock(Mutex *pm); // pm이 가리키는 뮤텍스에 잠금을 겁니다.
void unlock(Mutex *pm) // pm이 가리키는 해당 뮤텍스의 잠금을 풉니다.
|
그런데 뮤텍스 잠금을 관리하는 클래스를 하나 만들고 싶습니다. 이전에 걸어 놓은 뮤텍스 잠금을 잊지 않고 풀어 줄 목적인 거죠.
이런 용도의 클래스는 기본적으로 RAII 법칙을 따라 구성합니다. 즉, 생성 시에 자원을 획득하고, 소멸 시에 그 자원을 해제하는 것입니다.
1
2
3
4
5
6
7
8
9
|
class Lock {
public :
explicit Lock(Mutex *pm) : mutexPtr(pm) { lock(mutexPtr); } // 자원을 획득합니다.
~Lock() { unlock(mutexPtr); } // 자원을 해제합니다.
private :
Mutex *mutexPtr;
};
Colored by Color Scripter
|
사용자는 Lock를 사용할 떄 RAII 방식에 맞추어 쓰면 됩니다.
1
2
3
4
5
6
|
Mutex m; // 사용하고 싶은 뮤텍스를 정의합니다.
...
{ // 임계 영역을 정하기 위해 블록을 만듭니다.
Lock m1(&m); // 뮤텍스에 잠금을 겁니다.
... // 임계 영역에서 할 연산을 수행합니다.
} // 블록의 끝입니다. 뮤텍스에 걸렸던 잠금이 자동으로 풀립니다.
|
여기까지만 보면 앞으로도 잘 될 것 같습니다. 그런데 Lock 객체가 복사된다면 어떻게 해야 할까요?
1
2
|
Lock m11(&m); // m에 잠금을 겁니다.
Lock m12(m11); // m11을 m12로 복사합니다. 어떻게 되어야 맞을까요?
|
RAII 객체가 복사될 때 어떤 동작이 이루어져야 할까요?
아마도 열에 아홉은 다음의 네 가지 선택지 중 하나를 골라 잡고 싶을 겁니다.
첫 번째, 복사를 금지합니다.
실제로 RAII 객체가 복사되도록 놔두는 것 자체가 말이 안 되는 경우가 꽤 많습니다. 위의 Lock 같은 클래스도 이런 부류에 속할 것 같습니다.
복사를 막는 방법은 복사 연산을 private 멤버로 만드는 것입니다.
1
2
3
4
|
class Lock : private Uncopyable {
public :
... // 이전과 같습니다.
};
Colored by Color Scripter
|
두 번째, 관리하고 있는 자원에 대해 참조 카운팅을 수행합니다.
자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 저 세상으로 안 보내는게 바람직할 경우도 종종 있습니다. 이럴 경우에는, 해당 자원을 참조하는 객체의 개수에 대한 카운트를 증가시키는 식으로 RAII 객체의 복사 동작을 만들어야 합니다. 참고로, 이런 방식은 현재 tr1::shared_ptr이 사용하고 있습니다.
지금 좋은 생각이 떠오른 분이 있을 것 같네요. 자신의 RAII 클래스에 참조 카운팅 방식의 복사 동작을 넣고 싶을 때 tr1::shared_ptr을 데이터 멤버로 넣으면, 간단히 해결되겠죠? 그러니까 Lock이 참조 카운팅 방식으로 돌아가면 좋을 것 같다고 생각했다면, mutexPtr의 타입을 Mutex*에서 tr1::shared_ptr로 바꾸라는 것입니다. 단, 아쉽게도 tr1::shared_ptr은 참조 카운트가 0이 될 때 자신이 가리키고 있던 대상을 삭제해 버리도록 기본 동작이 만들어져 있어서, 우리의 바람과는 다소 어긋납니다.
참으로 다행스러운 사실은 tr1::shared_ptr이 삭제자 지정을 허용한다는 것입니다. 여기서 삭제자란, tr1::shared_ptr이 유지하는 참조 카운트가 0이 되었을 때 호출되는 함수 혹은 함수 객체를 일컫습니다. 삭제자는 tr1::shared_ptr 생성자의 두 번째 매개변수로 선택적으로 넣어 줄 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
|
class Lock {
public :
explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) // shared_ptr을 초기화하는데, 가리킬 포인터로 Mutex 객체의 포인터를 사용하고 삭제자로 unlock 함수를 사용합니다.
{
lock(mutexPtr.get());
}
private :
std::tr1::shared_ptr mutexPtr; // 원시 포인터 대신에 shared_ptr을 사용합니다.
};
Colored by Color Scripter
|
세 번째, 관리하고 있는 자원을 진짜로 복사합니다.
때에 따라서는 자원을 원하는 대로 복사할 수도 있습니다. 이때는 '자원을 다 썼을 때 각각의 사본을 확실히 해제하는 것'이 자원 관리 클래스가 필요한 유일한 명분이 되는 것이죠. 자원 관리 객체를 복사하면 그 객체가 둘러싸고 있는 자원까지 복사되어야 합니다, 즉 '깊은 복사'를 수행해야 한다는 이야기입니다.
네 번째, 관리하고 있는 자원의 소유권을 옮깁니다.
그리 흔한 경우는 아니지만, 어떤 특정한 자원에 대해 그 자원을 실제로 참조하는 RAII 객체는 딱 하나만 존재하도록 만들고 싶어서, 그 RAII 객체가 복사될 때 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 할 경우도 살다 보면 생깁니다. 이런 스타일의 복사 동작이 auto_ptr입니다.
객체 복사 함수는 컴파일러에 의해 생성될 여지가 있기 때문에, 컴파일러가 생성한 버전의 동작이 여러분이 원한 바와 맞지 않으면, 여러분이 객체 복사 함수를 직접 만들 수밖에 없습니다.
꼭 잊지 말아야 할 것!
1. RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정됩니다.
2. RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리하는 것입니다. 하지만 이 외의 방법들도 가능하니 참고해 둡시다.
'언어 > C++' 카테고리의 다른 글
[Effective C++] new 및 delete를 사용할 때는 형태를 반드시 맞추자 (0) | 2020.01.16 |
---|---|
[Effective C++] 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2020.01.15 |
[Effective C++] 자원 관리에는 객체가 그만! (0) | 2020.01.13 |
[Effective C++] 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2020.01.12 |
[Effective C++] operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2020.01.11 |