3. Item 13 : 자원관리는 객체로!
[ 자원 관리를 확실하게 하기위해선 객체를 이용한다 ]
싱글톤 패턴을 생각해보자.
사용자가 GetInstance를 하고 객체를 받아오면, 해제는 누가 할 것인가?
만약 사용자가 한다면 delete를 이용할 것이다. 그런데 delete 이전에 Error가 발생한다면?
해제가 제대로 되지 않을 경우가 생긴다. 이를 사전에 방지하기 위해서 소멸자를 이용한다.
즉, 자원을 관리하는 객체를 이용하는 것이다.
표준라이브러리에는 좋은 클래스가 많다.
4. Item 13 : 자원관리는 객체로!
[ auto_ptr ]
void f() {
std::auto_ptr<객체이름> ptrEX( GetInstance() ); // RAII
…
}
과 같이 이용한다. 이 함수를 빠져나가는 순간 소멸자가 호출되어 delete된다.
RAII는 자원을 할당한 순간 바로 자원관리 객체에게 넘겨주자는 것이다.
auto_ptr을 이용하면 한 문장에서 자원할당과 객체를 이용한 초기화가 이뤄진다.
그런데 auto_ptr은 특이한 성질이 하나 있다. 바로 1개의 자원만 관리한다는 것이다.
5. Item 13 : 자원관리는 객체로!
[ auto_ptr은 관리객체의 복사를 허용하지 않는다 ]
auto_ptr를 가지고 복사생성자나 대입연산을 수행하면 대상 객체에 정보를 넘기고 원본은 NULL이 된다.
즉, 하나의 자원밖에 가질 수 없다. 여러 자원들을 가지고 싶은 경우에는 shared_ptr을 사용한다.
[ shared_ptr ]
shared_ptr은 RCSP(참조 카운팅 방식의 Smart Pointer) 이다.
즉, 어떤 자원을 가리키는 외부 객체의 수를 유지하다가 그 개수가 0이 되면 자동으로 삭제한다.
auto_ptr과는 달리 복사를 허용한다.
6. Item 13 : 자원관리는 객체로!
[ 배열자원은 auto_ptr / shared_ptr 사용금지 ]
이 두개의 스마트 포인터는 소멸자에서 단순히 delete만 수행한다.
따라서 delete []가 되지 않으므로, 배열자원을 사용하면 안된다.
차라리 std::vector와 같은 Container 객체를 사용하는 것이 좋다.
7. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
모든 자원이 힙에서 생기는 것이 아니다.
힙에서 생기지 않는 자원은 스마트 포인터에 맞지 않는다.
예를 들어 Mutex의 Lock을 관리하는 클래스를 생각해보자.
class Lock{
public : 생성자 : lock 수행
소멸자 : unlock 수행
private :Mutex * mtxPtr;
};
8. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
별로 문제가 없을 것 같이 생겼다. 그런데 크나큰 문제가 있다. 바로 Lock 자원 객체의 복사이다.
Mutex를 복사한다면 많은 문제가 생길 수 있다.
- 서로 다른 Lock들이 서로에게 영향을 받을 수 있다.
- 따라서 원하지 않는 타이밍에 Unlock이 될 수 있다.
9. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
따라서 우리는 4가지 선택을 할 수 있다.
복사 금지
Mutex는 복사가 일어나면 안되므로 금지시킨다. Item 6처럼 복사를 봉인. (private에 복사연산 선언)
10. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
관리 자원의 참조 카운팅
관리 자원의 포인터를 shared_ptr을 이용하는 것이다.
즉, 원 관리 자원 포인터인 Mutex * 을 std::shared_ptr<Mutex>로 사용하는 것이다.
하지만 shared_ptr은 카운터가 0이 되면 가리키는 대상을 삭제하므로 문제가 된다.
단지 Mutex를 다 쓰면 잠금 해제만을 수행해야 한다.
그 때는 shared_ptr의 deleter를 사용하자.
11. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
관리 자원의 참조 카운팅
Lock (Mutex* pm) : mutrxPtr( pm, unlock ) { lock( mutexPtr.get() ); }
unlock이 바로 deleter로 등록된 것이다. deleter는 생성자의 두 번째 멤버로 쓰인다.
만약 참조 카운터가 0이 되면 관리자원의 deleter를 알아서 호출한다.
위 예제는 deleter가 unlock이므로, 0이 되면 unlock이 호출된다.
12. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
진짜로 복사하기
때에 따라서 그냥 관리 자원을 그대로 복사할 수 있다.
대신 자원 관리 객체를 복사하려면, 그 객체가 둘러싸고 있는 자원까지 복사해야 한다.
즉, 깊은 복사를 수행해야 한다.
13. Item 14 : 자원관리 클래스의 복사는 진지하게!
[ 힙이 아닌 자원들의 관리는? ]
관리하고 있는 자원의 소유권을 옮기자
자원을 참조하는 객체가 딱 하나만 존재해야 하는 경우가 있을 수 있다. (흔하지는 않다)
이 때는 그 자원의 소유권을 사본 쪽으로 옮겨야만 한다. (auto_ptr의 복사와 동일)
14. Item 15 : 관리자원은 외부에서 접근할 수 있도록!
[ 스마트 포인터로 직접 자원에 접근할 수 있다 ]
int daysHeld( const Investment * pi) 란 메소드를 생각해보자.
우리는 shared_ptr인 pInv로 이 메소드에 접근하고 싶다.
하지만 데이터 타입이 다르므로 접근이 되지 않는다. 따라서 적절한 형변환이 필요하다.
형변환 방법은 2가지가 있다.
명시적 변환
스마트 포인터는 get 함수가 있어, 객체의 포인터로 변환할 수 있다. pInv.get()
또한, 역참조 (->) 연산자도 오버로딩이 되어있어 자유롭게 접근이 가능하다.
15. Item 15 : 관리자원은 외부에서 접근할 수 있도록!
[ 스마트 포인터로 직접 자원에 접근할 수 있다 ]
암시적 변환
명시적으로 get을 사용하는 것이 좋지만, 변환할 때마다 함수 호출이 잦은 것이 싫을 수 있다.
그 때는 암시적 변환 함수를 사용하면 된다.
FontHandle get() const { return f; }
-> operator FontHandle() const { return f; }
편하지만 암시적 형변환은 실수의 여지가 많아 조심하는 것이 좋다.
16. Item 16 : new 및 delete는 형태를 맞춰서!
[ new의 동작 ]
1. 메모리가 할당된다.
2. 할당된 메모리에 대해 한 개 이상의 생성자가 호출된다.
[ delete의 동작 ]
1. 기존에 할당된 메모리에 대해 한 개 이상의 소멸자가 호출된다.
2. 메모리가 해제된다.
Delete 연산자가 적용되는 객체의 수는 소멸자가 호출되는 횟수에 따라 결정된다.
17. Item 16 : new 및 delete는 형태를 맞춰서!
[ 단일 객체와 배열의 메모리 구조 ]
배열에 대해 delete를 진행하면, n이라는 배열 크기의 정보를 전해주어야 한다.
따라서 delete 뒤에 [] 을 붙이는 것이다. 그러면 n을 읽어서 배열 전체를 delete 한다.
Object Object Object …n
단일객체 배열
18. Item 16 : new 및 delete는 형태를 맞춰서!
[ new와 delete 사용시 약속 ]
new를 사용 했으면 delete를 사용해라. 배열이면 delete에도 []을 붙여라.
만약 typedef를 이용한다면, 배열인지 아닌지를 꼭 명시해라. delete 시에 실수할 수도 있다.
그냥 배열을 typedef으로 만들지 않는 것이 가장 좋다.
19. Item 17 : RAII !!!
[ new로 만들고 스마트 포인터에 저장하는 코드는 별도로 만들자 ]
int priority();
void processWidget (std::shared_ptr<Widget> pw, int priority);
이런 구성 속에서, 아래 처럼 실행했다면 컴파일 오류이다.
processWidget (new Widget, priority());
왜냐하면 shared_ptr의 생성자는 explicit 이기 때문에, 암시적 변환이 일어나지 않는다.
20. Item 17 : RAII !!!
[ new로 만들고 스마트 포인터에 저장하는 코드는 별도로 만들자 ]
processWidget (std::shared_ptr(new Widget), priority());
위 코드는 컴파일이 된다. 그러나 여전히 문제를 가지고 있다.
이 코드 구성은 자원을 흘릴 가능성이 있다. 한 번 코드 구성을 하나하나 따져보자.
21. Item 17 : RAII !!!
[ new로 만들고 스마트 포인터에 저장하는 코드는 별도로 만들자 ]
processWidget (std::shared_ptr(new Widget), priority());
new Widget 표현식을 실행하는 부분과 shared_ptr 생성자를 호출하는 부분, priority 계산으로 나눠진다.
우리는 당연하게 new -> 스마트 포인터의 생성자 호출 -> priority 계산을 원한다.
하지만 컴파일러가 꼭 그렇게 하리라는 보장은 없다.
최악의 경우 new -> priority -> 스마트 포인터 생성자 순으로 호출될 수 있다.
만약 priority에서 오류가 발생하면, 그대로 자원이 누수(new Widget 포인터가 유실)된다.
22. Item 17 : RAII !!!
[ new로 만들고 스마트 포인터에 저장하는 코드는 별도로 만들자 ]
std::shared_ptr<Widget> pw(new Widget);
processWidget( pw, priority() );
그래서 위 코드처럼 스마트 포인터에 저장하는 코드는 별도로 먼저 만드는 것이 좋다.
이 경우에는 자원 누수 가능성이 없다.