일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- reference
- 게임
- 비교 함수 객체
- lua
- 언리얼
- 반복자
- operator new
- 상속
- more effective c++
- Smart Pointer
- 암시적 변환
- 예외
- 다형성
- c++
- 오블완
- Effective c++
- UE4
- 메타테이블
- Vector
- virtual function
- 티스토리챌린지
- exception
- 루아
- 참조자
- effective stl
- 영화 리뷰
- 영화
- 스마트 포인터
- implicit conversion
- resource management class
Archives
- Today
- Total
스토리텔링 개발자
[More Effective C++] 10. 생성자 예외 처리 본문
728x90
항목 10. 생성자에서는 리소스 누수가 일어나지 않게 하자.
생성자 설계 예제
// 이미지 정보 클래스
class Image
{
public:
Image(const string& imageDataFileName);
...
};
// 오디오 정보 클래스
class AudioClip
{
public:
AudioClip(const string& audioDataFileName);
...
};
// 전화번호 클래스
class PhoneNumber { ... };
// 주소록에 들어가는 하나의 정보에 대한 클래스
class BookEntry
{
public:
BookEntry(const string& name,
const string& address = "",
const string& imageFileName = "",
const string& audioClipFileName = "");
~BookEntry();
void addPhoneNumber(const PhoneNumber& number);
...
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
Image* theImage;
AudioClip* theAudioClip;
};
// 생성자와 소멸자
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
if(imageFileName != "")
theImage = new Image(imageFileName);
if(audioClipName != "")
theAudioClip = new AudioClip(audioClipFileName);
}
BookEntry::~BookEntry()
{
delete theImage;
delete theAudioClip;
}
- 정상적인 설계로 보이지만...
- theAudioClip을 할당하는 지점에서 메모리 할당이 실패한다면?(항목 8 참조)
- AduioClip 생성자 자체에서 예외를 발생시킨다면?
- audioClipName에서 예외가 발생한다면 이미 할당된 theImage 객체는 누가 삭제해 주어야 할까.
- BookEntry의 소멸자?
- 하지만 이 경우 BookEntry의 소멸자는 절대로 호출되지 않는다.
- 왜냐하면 BookEntry는 생성 과정이 완료되지 않았기 때문이다.
- BookEntry의 소멸자?
- C++는 생성 과정이 완료된(fully constructed) 객체만을 안전하게 소멸시킨다.
- 헌데, 생성자가 실행을 마치기 전에는 생성 작업이 완료된 걸로 간주되지 않는다.
- 예외 발생 시점에서, BookEntry는 생성 완료되지 않았으므로 소멸자는 호출되지 않는다.
- 즉, 소멸자에서 theImage 객체를 정리해줄 순 없다.
- 만약 예외를 잡아서 강제로 삭제해준다면 어떨까?
void testBookEntryClass()
{
BookEntry* pb = 0;
try
{
pb = new BookEntry("Addison-Wesley Publishing Company",
"One Jacob Way, Reading, MA 01867");
...
}
catch(...)
{
delete pb; // 예외 발생 시 pb 삭제.. 하지만 유효하지 않다.
throw;
}
delete pb; // 정상적으로 pb 삭제
- 왜냐하면 new 연산이 성공적으로 끝나기 전에는 pb에 대해 포인터 대입이 이루어지지 않기 때문이다.
- 그렇기에 pb를 스마트 포인터로 만드는 것도 도움이 되지 않는다.(항목 9 참조)
- 여전히 new 연산이 실패하기에 할당되지 않는다.
C++이 생성 과정이 완료되지 않은 객체에 소멸자 호출을 거부하는 이유
- 대부분의 경우 이차적으로 허용되지 않는(어쩌면 피해를 입힐 수 있는) 동작이기 때문이다.
- 생성 과정을 끝내지 못한 객체의 소멸자 동작을 억지로 해보자면..
- 생성자가 어떻게 실행되었는지 알려주는 객체를 두고,
- 그 객체에 생성자의 실행 상태를 알려주는 비트를 추가한다.
- 소멸자는 그 비트를 점검해서 자신이 취할 행동을 결정한다.
- 하지만 부득이하게 오버헤드가 발생한다.
- 생성자가 너무 느려지고 객체의 크기가 커진다.
- 그러므로 C++는 이러한 오버헤드를 피하고, 생성 중 중단된 객체가 자동으로 소멸되지 않는 것에 대한 부담을 프로그래머가 지게 했다.
- 즉 사용자가 직접 생성자를 설계해야 한다.
해결 방법
- 생성자에서 가능한 모든 예외를 받아서 마무리 코드를 실행하여 정리한 후, 예외를 전파 시킨다.
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
try
{
if(imageFileName != "")
{
theImage = new Image(imageFileName);
}
if(audioClipFileName != "")
{
thisAudioClip = new AudioClip(audioClipFileName);
}
}
catch(...) // 모든 예외를 받음
{
delete theImage;
delete theAudioClip; // 필요한 마무리 동작을 취한다.
throw; // 받은 예외를 다시 전파
}
}
- 데이터 멤버 중 포인터가 아닌 것은 어떻게 해야 할까?
- 클래스 생성자가 호출되기 전, 초기화리스트에서 이미 초기화가 이루어진다.
- 즉 생성자에서는 이미 생성 완료 상태이다.
- 그러므로 이들은 BookEntry 객체 소멸 시 자동으로 소멸한다.
- 이들이 예외를 발생시킬 수도 있지 않을까?
- 하지만 그건 그 객체의 생성자의 일이지, BookEntry 생성자가 신경쓸 일은 아니다.
- delete 문이 중복되는 것이 마음에 들지 않으므로.. 코드 중복 제거 버전
class BookEntry
{
public:
...
private:
...
void cleanup();
};
void BookEntry::cleanup()
{
delete theImage;
delete theAudioClip;
}
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
try
{
...
}
catch(...) // 모든 예외를 받음
{
cleanup(); // 리소스 해제
throw; // 받은 예외를 다시 전파
}
}
BookEntry::~BookEntry()
{
cleanup(); // 리소스 해제
}
심화
- 만일 포인터(theImage, theAudioClip)가 상수 포인터라면?
class BookEntry
{
public:
...
private:
...
Image* const theImage;
AudioClip* const theAudioClip; // 포인터가 상수가 된다.
};
- 포인터 상수는 상수이므로, 초기화 리스트를 통해 초기화 되어야 한다.
- 하지만 그렇다고 해서 아래처럼 하면 안 될 것이다.
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName : 0)
{ }
- 문제점
- 이전 코드와 문제점을 공유한다.
- theAudioClip 생성 중 예외가 발생한다면 theImage의 정리는 누가 맡지?
- 심지어 이번에는 try-catch 문을 심을 수조차 없다.
- 결국 try-catch문을 넣을 다른 방도를 찾아야 한다.
- 해결법1. 초기화된 포인터값을 반환하는 private 멤버 함수를 만든다.
class BookEntry
{
public:
...
private:
...
Image* initImage(const string& imageFileNmae);
AudioClip* initAudioClip(const string& audioClipFileName);
};
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(initImage(imageFileNmae)),
theAudioClip(initAudioClip(audioClipFileNmae))
{}
// theImage가 먼저 초기화되므로 리소스 누수는 신경쓰지 않아도 된다.
Image* BookEntry::initImage(const string& imageFileName)
{
if(imageFileName != "") return new Image(imageFileName);
else return 0;
}
// theImage 리소스를 해제하는 예외 처리가 필요하다.
AudioClip* BookEntry::initAudioClip(const string& audioClipFileName)
{
try
{
if(audioClipFileName != "")
return new AudioClip(audioClipFileName);
else
return 0;
}
catch(...)
{
delete theImage;
throw;
}
}
- 하지만 단점도 있다.
- 개념상 생성자가 있어야 할 코드가 쪼개져 있으므로 유지보수가 힘들다.
- 해결법 2. theImage와 theAudioClip을 포인터가 아니라 자원관리객체로 만든다. (항목 9 참조)
class BookEntry
{
public:
...
private:
...
const auto_ptr<Image> theImage; // 스마트 포인터로 변경
const auto_ptr<AudioClip> theAudioClip;
};
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioClipFileName != "" ? new AudioClip(autioClipFileName) : 0)
// theAudioClip에서 예외가 발생해도
// theImage는 이미 객체로 만들어져 있으므로 소멸될 때 자동으로 소멸된다.
{}
BookEntry::~BookEntry()
{} // 할 것이 전혀 없다!
- 메모리 누수도 일으키지 않고, 멤버 초기화 리스트를 통해 초기화가 가능해진다.
728x90
'개발 > More Effective C++' 카테고리의 다른 글
[More Effective C++] 12. 예외 전달 vs 함수 매개변수 전달 (0) | 2024.08.14 |
---|---|
[More Effective C++] 11. 소멸자 예외 처리 (0) | 2024.08.13 |
[More Effective C++] 9. 자원 관리 객체(RAII) (0) | 2024.08.09 |
[More Effective C++] 8. new / delete 연산자와 operator new / delete (0) | 2024.08.08 |
[More Effective C++] 7. operator&& / operator|| / operator, (0) | 2024.08.07 |
Comments