Effective C++/More Effective C++
[More Effective C++] 10. 생성자 예외 처리
김디트
2024. 8. 12. 11:33
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