◆ 무한한 가능성/& Visual C/C++

[요약] Effective C++ 3장 (항목13~17)

치로로 2010. 7. 30. 16:08
 
[요약] Effective C++ 3장 (항목13~17)



3장 자원 관리



항목 13: 자원 관리에는 객체가 그만!



투자에 대한 최상위 클래스 라이브러리가 있다고 가정하자.
1: class Investment { ... };               // 여러 형태의 투자를 모델링한
2:                                         // 클래스 계통의 최상위 클래스

이 라이브러리에 Investment에서 파생된 클래스의 객체를 사용자가 얻어내는 용도로
팩토리 함수(항목 7참조)만을 쓰도록 만들어져 있다고 하자.
1: Investment* createInvestment();               // Investment 클래스 계통에 속한 클래스의 객체를 
2:                                               // 동적할당하고 그 포인터를 반환한다.
3:                                               // 이 객체의 해제는 호출자 쪽에서 직접 해야한다.

createInvestment 함수를 통해 얻어낸 객체를 사용할 일이 이제 없을 때,
그 객체를 삭제해야 하는 쪽은 이 함수의 호출자(caller)이다.

추가함수로 아래의 f() 함수를 보자. 이것이 호출자가 객체를 삭제하는 함수이다.
1: void f()
2: {
3:         Investment *pInv = createInvestment();        // 팩토리 함수를 호출
4:         ...                                           // pInv를 사용
5:         delete pInv;                                  // 객체를 해제
6: }


위의 f() 함수가 정상적으로 보이나, createInvestment 함수로부터 얻은 투자 객제의 삭제에 실패할 수 있는
경우의 수가 한두개가 아니다.

... 부분에서 return 이 들어갈 수도 있고, 그럼 delete의 라인까지 가지못하게 된다.
또한 중간에 continue 나 goto 나 비정상적인 loop 문이 있다면, 물론 또 delete pInv를 할 수 없게 된다.
이렇게 되면 프로그램은 비정상적인 수행을 하게되므로 문제가 된다.

createInvestment 함수로 얻어낸 자원이 항상 해제되도록 만들 방법은, 자원을 객체에 넣고 그 자원 해제를
소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 f를 떠날때 호출되도록 만드는 것이다.

추가적으로, C++은 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있다.
이는 후에 설명.

SW개발에 쓰이는 상당수의 자원이 힙에서 동적으로 할당되고, 하나의 block 혹은 function 안에서만
쓰이는 경우가 잦기 때문에 그 block or function로부터 실행 제어가 빠져나올때 자원이 해제 되는게 맞다.

표준 라이브러리를 보면 auto_ptr이 있다.
이는 자동으로 객체를 해제시켜주는 Smart Pointer 이다.
소멸자가 자동으로 delete를 불러주도록 설계되어 있다.

그럼, f() 함수에 대한 추가적 예제를 보자.

1: void f()
2: {
3:         std::auto_ptr<Investment> pInv(createInvestment());         // 팩토리 함수를 호출
4:         ...                                                         // pInv 사용
5: }                                                             // 소멸자를 통해 pInv 삭제

아주 간단한 예지지만, 자원 관리에 객체를 사용하는 방법의 중요 두 가지 특징을 여기서 찾을 수 있다.


* 첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
 : createInvestment 함수가 만들어 준 자원은 그 자원을 관리할 auto_ptr 객체를 초기화 하는데 쓰인다.
  자원 관리 획득 즉 초기화(RAII:resource Acquisition Is Initialization), 이는 자원 획득과 자원 관리 객체의
  초기화가 바로 한 문장에서 이루어지는 것이 너무나도 일상적이기 때문이다.

* 둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다. :
 : 소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에
  상관없이 자원 해제가 제대로 이루어지게 되는 것이다.  객체를 해제하다 예외가 발생되면 항목8처럼 해주면 된다.


auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 하기 때문에,
어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 절대로 안된다.
정.말.중.요.
만약 이런 사태가 발생되면, 결국 자원이 두 번 삭제되는 결과를 낳으므로...
프로그램은 안드로메다로 향하게 된다.

이런 불상사를 막기 위해 auto_ptr은 특이한 특성을 지니게 되었는데, 그것은 auto_ptr객체를
복사하면(복사생성자 or 복사대입연산자를 통해) 원본 객체는 null로 만든다.
복사하는(copy) 객체만이 그 자원의 유일한 소유권(ownership)을 갖는다고 가정한다.

1: std::auto_ptr<Investment>                // pInv1이 가리키는 대상은
2:         pInv1(createInvestment());             // createInvestment 함수에서 반환된 객체이다.
3: std::auto_ptr<Investment> pInv2(pInv1);  // pInv2는 현재 그 객체를 가리키고 있는 반면, pInv1은 null.
4:   pInv1 = pInv2;                         // pInv1은 객체를 가리키고, pInv2는 null.
5:                                           


이렇게 일반적이지 않은 copy를 수행한다는 점을 잊지 말자.

만약, auto_ptr을 쓸 수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터
(RCSP: Reference-Counting Smart Pointer)이 가장 괜찮은 대안이다.

RCSP는 특정한 어떤 자원을 참조하는 외부 객체의 개수를 유지하고 있다가 그 개수가 '0'이 되면
해당 자원을 자동으로 삭제하는 스마트 포인터 이다.
이것만 보면, RCSP의 동작은 가비지 컬렉션(garbage collection)과 상당히 흡사하다.
단, 참조 상태가 고리를 이룰경우(서로 참조함) 없앨 수 없다는 점은 가비지 컬렉션과 다르다.

TR1에서 제공되는 tr1::shared_ptr(항목 54참조)이 대표적인 RCSP 이다.
f() 함수를 이용하여 다시 보자.

1: void f()
2: {
3:         ...
4:         std::tr1::shared_ptr<Investment>
5:                 pInv(createInvestment());                  // 팩토리 함수를 호출한다.
6:         ...                                          // 예전처럼 pInv를 사용한다.
7: }                                              // shared_ptr의 소멸자를 통해
8:                                                // pINv를 자동으로 삭제한다.


auto_ptr을 사용한 버전과 거의 똑같아 보이는 코드이지만, shared_ptr의 복사가 훨씬 자연스럽다.

01: void f()
02: {
03:         ...
04:         std::tr1::shared_ptr<Investment>             // pInv1이 가리키는 대상은
05:                 pInv(createInvestment());                  // createInvestment()에서 반환된 객체
06: 
07:         std::tr1::shared_ptr<Investment>             // pInv1 및 pInv2가 동시에
08:                 pInv2(pInv1);                              // 그 객체를 가리키고 있다.
09:         pInv1 = pInv2;                               // 마찬가지 - 변한 것은 하나도 없다.
10: 
11:         ...                                          // pInv1 및 pInv2는 소멸되며,
12: }                                              // 이들이 가리키고 있는 객체도
13:                                                // 자동으로 삭제된다.

복사 동작이 상식적으로 진짜 복사(copy)가 되기 때문에, tr1::shared_ptr은
괴상한 복사 동작으로 인해 auto_ptr을 쓸 수 없는 STL 컨테이너 등의 환경에 알맞게 슬 수 있다.

다시 처음으로 돌아와서... 진짜 하고자 하는 이야기는......
자원을 관리하는 객체를 써서 자원을 관리하는 것이 중요하다는 것이다.

auto_ptr 과 tr1::shared_ptr은 그렇게 하는 여러가지 방법들 중 몇가지일 뿐이다.
(tr1::shared_ptr 클래스에 대한 더 자세한 이야기는 항목 14, 18, 54 참고)

추가적으로... 이것도 알아두어야 한다.
auto_ptr 및 tr1::shared_ptr은 소멸자 내부에서 delete 연산자를 사용한다.
delete[] 연산자가 아니다. (이 둘의 차이는... 항목 16 확인)

말하자면, 동적할당 배열에 대해 auto_ptr이나 tr1::shared_ptr을 사용하면 대략난감 이란 말이다.
동적배열을 썻을 때 커파일 에러도 안나므로 잘 확인해야 한다.
1: std::auto_ptr<std::string>                        // 좋지 않은 발상이다!
2:   aps(new std::string[10]);                       // 잘못된 delete 가 사용된다.
3: 
4: std::tr1::shared_ptr<int> spi(new int[1024]);     // 같은 문제가 발생한다.

그리고
C++ 표준 라이브러리에서는 동적할당된 배열을 위해 준비된 auto_ptr 혹은 tr1::shared_ptr 같은
클래스가 제공되지 않는다. TR1에서도 제공되지 않는다.

왜냐하면 동적할당된 배열은 이제 vector 및 string으로 거의 대체할 수 있기 때문이다.
배열에 쓸 수 있는 auto_ptr이라든지 tr1::shared_ptr이 있으면 좋겠다는 분은 부스트(항목 55 번)를 참조하면 된다.
원하는 기능을 정확히 가지고 있는 boost::scoped_array 와 boost::shared_array가 있다.

이번 항목에서 강조하고 싶은 것은...
자원 해제를 여러분이 일일이 하다 보면(자원 관리 클래스를 쓰지 않고 delete를 쓴다든지 해서)
언젠가 잘못을 저지르고 만다는 이야기이다. 이미 널리 쓰이고 있는 auto_ptr이나 tr1::shared_ptr 같은
자원 관리 클래스를 활용하는 것도 이번 항목의 조언을 쉽게 지킬 수 있는 한 가지 방법이다.

마지막으로...

앞에서 본 createInvesetment 함수의 반환 타입이 포인터로 되어 있는데,
이 부분 때문에 문제가 생길 수 있음을 지적하고 싶다.
반환된 포인터에 대한 delete 호출을 호출자 쪽에서 해야 하는데, 그것을 잊어버리고 넘어가기
쉽기 때문이다.(auto_ptr 혹은 tr1::shared_ptr을 써서 delete를 수행한다 하더라도,
createInvestment의 반환 값을 스마트 포인터에 저장해야 한다는 점만은 여전히 기억하고 있어야 한다.)
이 문제를 어떻게든 해결하려면 createInvestment 를 수술해서 인터페이스를 고쳐야 하는데,
항목 18에서 다루도록 한다.



// 막상 쓰다보니......... 요약이 아니네요 -_-;;;;;; 죄송합니다.ㅎㅎㅎ
// 이번장은 요약하자면... auto_ptr은 복사가 아닌 이동과 같은 것들이고, tr1::shared_ptr 이 복사 기능이다.


 *이것만은 잊지 말자!

  ** 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고
     소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
  ** 일반적으로 널리 쓰이는 RAII 클래스는 tr1::shared_ptr 그리고 auto_ptr 이다.
     이 둘 가운데 tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋다.
     반면, auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버린다.







항목 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자.














............작...성...중.................................................................


 *이것만은 잊지 말자!

  ** 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을
     빠뜨리지 말고 복사해야 한다.
  ** 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는
     시도는 절대로 하지 말자.  그 대신, 공통된 동작을 제3의 함수에다 분리해놓고
     양쪽에서 이것을 호출하게 만들어서 해결하자.














※ Effective C++ 의 책을 개인적으로 요약한 것입니다.
책의 저작권 등등 각종 권한은 출판사와 지은이/옮긴이에 있음
 - 출판: Addison Wesley
 - 지음: 스콧 마이어스
 - 옮김: 곽용재





'◆ 무한한 가능성 > & Visual C/C++' 카테고리의 다른 글

[C++] WM_PAINT - line, rectangle, ellipse, star, radian, textout  (1) 2012.03.16
[C++] Debug 참고  (0) 2012.03.15
waveOut~()  (0) 2010.03.19
waveIn~()  (0) 2010.03.16
저수준 멀티미디어 API함수의 이해  (0) 2010.03.16