[요약] Effective C++ 3장 (항목13~17)
3장 자원 관리
항목 13: 자원 관리에는 객체가 그만!
투자에 대한 최상위 클래스 라이브러리가 있다고 가정하자.
이 라이브러리에 Investment에서 파생된 클래스의 객체를 사용자가 얻어내는 용도로
팩토리 함수(항목 7참조)만을 쓰도록 만들어져 있다고 하자.
createInvestment 함수를 통해 얻어낸 객체를 사용할 일이 이제 없을 때,
그 객체를 삭제해야 하는 쪽은 이 함수의 호출자(caller)이다.
추가함수로 아래의 f() 함수를 보자. 이것이 호출자가 객체를 삭제하는 함수이다.
위의 f() 함수가 정상적으로 보이나, createInvestment 함수로부터 얻은 투자 객제의 삭제에 실패할 수 있는
경우의 수가 한두개가 아니다.
... 부분에서 return 이 들어갈 수도 있고, 그럼 delete의 라인까지 가지못하게 된다.
또한 중간에 continue 나 goto 나 비정상적인 loop 문이 있다면, 물론 또 delete pInv를 할 수 없게 된다.
이렇게 되면 프로그램은 비정상적인 수행을 하게되므로 문제가 된다.
createInvestment 함수로 얻어낸 자원이 항상 해제되도록 만들 방법은, 자원을 객체에 넣고 그 자원 해제를
소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 f를 떠날때 호출되도록 만드는 것이다.
추가적으로, C++은 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있다.
이는 후에 설명.
SW개발에 쓰이는 상당수의 자원이 힙에서 동적으로 할당되고, 하나의 block 혹은 function 안에서만
쓰이는 경우가 잦기 때문에 그 block or function로부터 실행 제어가 빠져나올때 자원이 해제 되는게 맞다.
표준 라이브러리를 보면 auto_ptr이 있다.
이는 자동으로 객체를 해제시켜주는 Smart Pointer 이다.
소멸자가 자동으로 delete를 불러주도록 설계되어 있다.
그럼, f() 함수에 대한 추가적 예제를 보자.
아주 간단한 예지지만, 자원 관리에 객체를 사용하는 방법의 중요 두 가지 특징을 여기서 찾을 수 있다.
* 첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
: createInvestment 함수가 만들어 준 자원은 그 자원을 관리할 auto_ptr 객체를 초기화 하는데 쓰인다.
자원 관리 획득 즉 초기화(RAII:resource Acquisition Is Initialization), 이는 자원 획득과 자원 관리 객체의
초기화가 바로 한 문장에서 이루어지는 것이 너무나도 일상적이기 때문이다.
* 둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다. :
: 소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에
상관없이 자원 해제가 제대로 이루어지게 되는 것이다. 객체를 해제하다 예외가 발생되면 항목8처럼 해주면 된다.
auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 하기 때문에,
어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 절대로 안된다.
정.말.중.요.
만약 이런 사태가 발생되면, 결국 자원이 두 번 삭제되는 결과를 낳으므로...
프로그램은 안드로메다로 향하게 된다.
이런 불상사를 막기 위해 auto_ptr은 특이한 특성을 지니게 되었는데, 그것은 auto_ptr객체를
복사하면(복사생성자 or 복사대입연산자를 통해) 원본 객체는 null로 만든다.
복사하는(copy) 객체만이 그 자원의 유일한 소유권(ownership)을 갖는다고 가정한다.
이렇게 일반적이지 않은 copy를 수행한다는 점을 잊지 말자.
만약, auto_ptr을 쓸 수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터
(RCSP: Reference-Counting Smart Pointer)이 가장 괜찮은 대안이다.
RCSP는 특정한 어떤 자원을 참조하는 외부 객체의 개수를 유지하고 있다가 그 개수가 '0'이 되면
해당 자원을 자동으로 삭제하는 스마트 포인터 이다.
이것만 보면, RCSP의 동작은 가비지 컬렉션(garbage collection)과 상당히 흡사하다.
단, 참조 상태가 고리를 이룰경우(서로 참조함) 없앨 수 없다는 점은 가비지 컬렉션과 다르다.
TR1에서 제공되는 tr1::shared_ptr(항목 54참조)이 대표적인 RCSP 이다.
f() 함수를 이용하여 다시 보자.
auto_ptr을 사용한 버전과 거의 똑같아 보이는 코드이지만, shared_ptr의 복사가 훨씬 자연스럽다.
복사 동작이 상식적으로 진짜 복사(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을 사용하면 대략난감 이란 말이다.
동적배열을 썻을 때 커파일 에러도 안나므로 잘 확인해야 한다.
그리고
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
- 지음: 스콧 마이어스
- 옮김: 곽용재
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 |