[Effective C++] 14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
항목 13에서 힙 기반 자원을 해제하는 법을 알아봤는데, 힙에 할당되지 않는 자원이라면 우리는 스스로 자원 관리 클래스를 만들어야 한다.
예를 들어 Mutex 타입의 객체를 조작하는 C API를 사용 중이라고 가정해보자.
해당 API에서 제공하는 함수 중엔 lock, unlock함수를 제공한다.
void lock(Mutex* pm); // pm이 가리키는 뮤텍스에 잠금을 건다.
void unlock(Mutex* pm); // pm이 가리키는 해당 뮤텍스의 잠금을 해제한다.
우리는 배운 것처럼 자원을 획득하고, 소멸 시에 그 자원을 해제해야 하니 이전에 걸어 놓은 뮤텍스 잠금을 잊지 않고 풀어 줄 목적인 뮤텍스 잠금을 관리하는 클래스를 하나 만든다고 생각해보자.
class Lock
{
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{
lock(mutexPtr); //자원 획득
}
~Lock()
{
unlock(mutexPtr); //자원 해제
}
private:
Mutex* mutexPtr;
};
사용자는 Lock을 사용할 때 RAII방식(객체의 생성과 동시에 자원을 할당하고 객체가 파괴될 때 자원을 해제하는 방법)에 맞춰 쓰면 된다.
{
Mutex m; //뮤텍스 정의
Lock m1(&m); //뮤텍스 잠금
} //자원 해제
그런데 여기서 Lock객체가 복사된다면 어떻게 될까?
Mutex m;
Lock m1(&m);
Lock m2(m1); //복사 생성자
RAII 객체의 복사 동작
RAII 객체가 복사될 때 어떤 동작이 이루어져야 하는가에 대한 선택지는 아래와 같다.
1. 복사를 금지한다.
RAII 객체가 복사되도록 놔두는 것 자체가 말이 안 되는 경우가 있다.
앞의 Lock클래스만 해도 스레드 동기화에 대한 객체의 사본은 의미가 없다.
복사를 막는 방법은 항목 6을 참고하면 되는데, 간단하게 말하면 복사 연산을 private 멤버로 만드는 것이다.
2. 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
자원을 갖고 있는 마지막 객체가 소멸될 때까지 그 자원을 해제하지 않는 게 바람직한 경우가 있는데, 이럴 경우에 해당 자원을 참조하는 객체의 개수에 대한 카운트를 증가시키는 식으로 RAII객체의 복사 동작을 만들어야 한다.
따라서 shared_ptr을 사용하게 되는데 방법은 간단하다. Lock클래스의 멤버 변수인 Mutex* mutexPtr을 shared_ptr <Mutex> mutexPtr로 바꾸면 된다.
하지만 shared_ptr은 참조 카운트가 0이 될 때 자원을 해제하기 때문에 Mutex가 삭제될 수 있는데, 이를 방지하기 위해 shared_ptr에 삭제자를 지정하면 된다.
shared_ptr의 삭제자란 참조 카운팅이 0이 되었을 때 호출되는 함수로 생성자의 두 번째 매개변수에 넣는다.
class Lock {
public:
explicit Lock(Mutext *pm) : mutexPtr(pm,unlock){ //삭제자로 unlock 함수 지정
lock(mutexPtr.get()); //get 에 관한것은 항목 15를 보자
}
//객체 소멸과정을 잊은게 아니라 컴파일러가 생성한 소멸자를 통해 동작한다.
//클래스의 소멸자는 비정적 데이터 멤버의 소멸자를 자동 호출하게 되어 있다.
private:
shared_ptr<Mutex> mutexPtr;
}
3. 관리하고 있는 자원을 진짜로 복사한다.
deep copy를 수행하자.
표준 string 타입을 구현해 놓은 것 중에, 문자열을 구성하는 원소들을 힙 메모리에 저장해 놓고
이 메모리에 대한 포인터를 데이터 멤버로 갖고 있는 경우가 있다.
이때 이 객체를 복사하면, 사본은 포인터 및 그 포인터가 가리기는 새로운 힙 메모리를 갖게 되는데 깊은 복사를 보여주는 한 예라고 볼 수 있다.
4. 관리하고 있는 자원의 소유권을 옮긴다.
그 자원을 실제로 참조하는 RAII객체를 딱 하나만 존재하도록 만든다. unique_ptr의 복사 동작에 해당되며, 이때의 복사는 복사가 아니라 이동이다.
요약
- RAII객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII객체의 복사 동작이 결정된다.
- RAII클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 선으로 마무리해주는 것이다. 하지만 이외의 방법들도 가능하니 참고하자.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자 (0) | 2022.05.25 |
---|---|
[Effective C++] 15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2022.05.22 |
[Effective C++] 13. 자원 관리에는 객체가 그만! (0) | 2022.05.20 |
[Effective C++] 12. 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2022.05.18 |
[Effective C++] 11. operator=에서는 자기 대입에 대한 처리가 빠지지 않도록 하자 (0) | 2022.05.18 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자
[Effective C++] 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자
2022.05.25 -
[Effective C++] 15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
[Effective C++] 15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
2022.05.22 -
[Effective C++] 13. 자원 관리에는 객체가 그만!
[Effective C++] 13. 자원 관리에는 객체가 그만!
2022.05.20 -
[Effective C++] 12. 객체의 모든 부분을 빠짐없이 복사하자
[Effective C++] 12. 객체의 모든 부분을 빠짐없이 복사하자
2022.05.18