스토리텔링 개발자

[More Effective C++] 10. 생성자 예외 처리 본문

개발/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는 생성 과정이 완료되지 않았기 때문이다.
  • 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++이 생성 과정이 완료되지 않은 객체에 소멸자 호출을 거부하는 이유
  • 대부분의 경우 이차적으로 허용되지 않는(어쩌면 피해를 입힐 수 있는) 동작이기 때문이다.
  • 생성 과정을 끝내지 못한 객체의 소멸자 동작을 억지로 해보자면..
    1. 생성자가 어떻게 실행되었는지 알려주는 객체를 두고,
    2. 그 객체에 생성자의 실행 상태를 알려주는 비트를 추가한다.
    3. 소멸자는 그 비트를 점검해서 자신이 취할 행동을 결정한다.
  • 하지만 부득이하게 오버헤드가 발생한다.
    • 생성자가 너무 느려지고 객체의 크기가 커진다. 
    • 그러므로 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
Comments