item 10 : 생성자에서는 리소스 누수가 일어나지 않게 하자.

멀티미디어 기능을 가진 주소록을 만든다고 하자.
주소록(BookEntry)은 이름, 주소, 전화번호 외에 사진이나 목소리 정보도 갖고 있다고 하자.


class Image {
public:
Image(const string& image_file_name);
...
};

class AudioClip {
public:
AudioClip(const string& audio_file_name);
...
}

class PhoneNumber {...};

class BookEntry {
public:
BookEntry(const string& name,
             const string& address = "",
             const string& imageFileName = "",
             const string& audioFileName = "");
~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& audioFileName)
: theName(name), theAddress(address),
theImage(0), theAudioClip(0)
{
if (imageFileName != "") {
theImage = new Image(imageFileName);
}

if (audioFileName != "") {
theAudioClip = new AudioClip(audioFileName);
}

}

BookEntry::~BookEntry() {
delete theImage;
delete theAudioClip;
}


별 문제없어 보이지만, 비정상적인 상태(예외가 발생한)에서는 별별 문제가 다 생긴다.

만약 오디오클립을 만들다가 예외가 발생한다면?

if (audioFileName != "") {
theAudioClip = new AudioClip(audioFileName);
}

new에서 메모리 할당을 실패했을 수도 있고, AudioClip 생성자 자체에서 예외를 발생시켰을 수도 있다.
어쨌든, 여기서 예외가 발생한 경우, 위에서 할당한 theImage는 어떻게 될까?

소멸자가 불려서 지워질까? NO.

C++에서는 생성과정이 완료된 객체만 안전하게 소멸시킨다.


void test() {
 BookEntry b("name", "addr");
}
b가 생성되는 도중에 예외가 발생하면 b의 소멸자는 불리지 않는다.

소멸자가 불리게하기 위해 다음의 코드를 작성한다면?
void test2() {
BookEntry *pb = 0;
try {
     pb = new BookEntry("name", "addr");
} catch(...) {
     delete pb; //예외발생시 pb삭제

     throw;     //예외전파.
}

...

delete pb;     //정상적인 pb삭제.
}

이렇게 해도 theImage 여전히 유실된다.
왜냐하면 new 연산이 성공적으로 끝나기 전에는 pb에 대해 포인터 대입이 이루어지지 않기 때문에.
delete pb해봐야 아무것도 안한다.

스마트포인터를 쓴다면? -> 역시 pb에 포인터대입이 되어있지 않기 때문에 아무 쓸데없다.

어째서 C++은 생성과정이 완료되지 않은 객체에 대해 소멸자 호출을 해주지 않아 우리를 힘들게하는가?
생성 과정을 끝내지 못한 객체에 대해 소멸자가 호출되었다고 가정하면, 이 소멸자는 어떤 동작을 취할지 어떻게 파악할 수 있을까?
더럽게 될 수 밖에 없다. 머리를 짜내 생각해봐도 궁여지책일 수 밖에 없음. 따라서 C++은 오버헤드를 피하고, 프로그래머에게 부담을 지도록 했다.

어쨌든 C++에서 생성자에서 예외가 발생한 객체에 대해 아무것도 안해주기 때문에 우리가 직접 코드를 짜야한다.

방법 중 하나는 try-catch를 사용해서 예외가 발생하면 catch해서 처리 후 전파하는 것이다.

BookEntry::BookEntry(const string& name,
                     const string& address,
                     const string& imageFileName,
                     const string& audioFileName)
: theName(name), theAddress(address),
theImage(0), theAudioClip(0)
{
     try {
          if (imageFileName != "") {
          theImage = new Image(imageFileName);
          }

          if (audioFileName != "") {
          theAudioClip = new AudioClip(audioFileName);
          } 
     } catch (...) {
          delete theImage;
          delete theAudioClip;

          throw;          //받은 예외 전파.
     }
}

BookEntry의 데이터 멤버 중에서 포인터가 아닌 것은 어쩌나??
-> 걱정할 필요 없다.
클래스 생성자가 호출되기 이전에 데이터 멤버들의 초기화가 이루어지기 때문이다.(?)

그럼 클래스 생성자가 호출되기 이전에 데이터 멤버들의 초기화를 위해 데이터 멤버의 클래스 생성자가 호출되는 과정에서 예외가 발생한다면?
-> 그 데이터 멤버 생성자가 알아서 처리해야한다. BookEntry의 소관이 아님.


그렇다면 theImage와 theAudioClip이 상수 포인터인 경우에는?

class BookEntry {
public:
...
private:
...
Image * const theImage;          //포인터가 가리키는 값은 변할 수 없다. 하지만 Image는 const가 아님.
AudioClip * const theAudioClip;
};

생성자 body에서 초기화 못함, 멤버 초기화 리스트에서 초기화 해줘야한다.

BookEntry::BookEntry(const string& name,
                     const string& address,
                     const string& imageFileName,
                     const string& audioFileName)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioFileName != "" ? new AudioClip(audilFileName) : 0)
{ }
이렇게 짜면?
처음에 짠거랑 똑같이 메모리 누수가 발생하는 코드가 된다!

그럼 try-catch를 붙이면 되는가?
-> 못붙임..

try-catch는 문장(statement)고, 멤버 초기화 리스트는 표현식(expression)만 가능하다.
(if else도 못쓴다)

그럼 어떻게해주느냐
파일네임을 받아서 각각의 Image, AudioClip 포인터를 리턴해주는 private 멤버 함수를 만든다.

class BookEntry {
public:
...
private:
...
Image* initImage(const string& imageFileName);
AudioClip* initAudioClip(const string& audioFileName);

};



BookEntry::BookEntry(const string& name,
                     const string& address,
                     const string& imageFileName,
                     const string& audioFileName)
: theName(name), theAddress(address),
theImage(initImage(imageFileName)),
theAudioClip(initAudioClip(audioFileName))
{ }

//theImage가 먼저 초기화되기 때문에,
//이 초기화가 실패하더라도 리소스 누수가 생길 걱정은 하지 않아도 된다.(?)
// (실패할 경우 생성자 로직 중단되는데, 이전에 리소스 할당한 거 아무것도 없기 때문에 걱정ㄴㄴ? theImage도 할당안된것이고?)
//따라서 이 함수는 예외를 발생시키지 않도록 작성.
Image* BookEntry::initImage(const string& imageFileName) {
    if (imageFileName != "")
          return new Image(imageFilename)
     else
          return 0;
}

//AudioClip은 두번째로 초기화되기 때문에
//여기서 예외발생하면 theImage를 반드시 해제해줘야한다.
AudioClip* BookEntry::initAudioClip(const string& audioFileName) {
     try {
          if (audioFileName != "") {
               return new AudioClip(audioClipFileName);
          }
     } catch (...) {
          delete theImage;
          throw;
     }
}

상당히 깔끔해졌을 뿐만 아니라, 우리가 풀려고 낑낑대었던 문제까지 한방에 해결해주는 멋진 클래스가 됨.(??)


더 좋은 방법이 있다.
theImage와 theAudioClip이 가리키는 객체를 지역 객체로 관리하는 리소스로 취급하는 것이다.(?)
auto_ptr!!

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& audioFileName)
: theName(name), theAddress(address),
theImage(imageFileName != "" ? new Image(imageFileName) : 0),
theAudioClip(audioFileName != "" ? new AudioClip(audilFileName) : 0)
{ }
이번엔 이렇게 짜도 누수없이 제대로 됨.
theImage와 theAudioClip이 단순한 포인터가 아닌 객체이기 때문에, 이제 정상적으로 자동소멸됨.

+ 이제 BookEntry소멸자에서 암것도 안해줘도된다.

포인터 클래스 멤버를 auto_ptr로 바꾸면,
생성자는 실행 도중에 예외가 발생해도 리소스 누수를 일으키지 않으며,
소멸자에서 직접 리소스 해제하지 않아도 되며,
상수 멤버 포인터도 비상수 멤버 포인터처럼 똑같이 깔끔하게 처리 가능.

예외 안정성 + 코드 가독성까지 선사하는 auto_ptr


item 11 : 소멸자에서는 예외가 탈출하지 못하게 하자.

클래스 소멸자가 호출되는 경우는 두가지이다.
첫번째는 scope을 벗어나 정상적으로 소멸되는 경우,
두번째는 예외 처리 매커니즘에 의해 객체가 소멸되는 것, (예외 전파(exception propagation) 과정의 일부분으로 스택되감기가 진행될때.)
스택되감기? catch를 만날때까지 호출자를 찾아돌아가는 과정?에서 스택에 있던 것들 전부 정리됨.

소멸자가 불릴때 정상적인 경우일 수도 있고, 예외가 발생한 경우일 수도 있다.
소멸자에서는 어떤 상황인지 구분할수 있는 방법이 없다. 따라서 방어적으로 코딩해야함!
예외에 대한 처리가 안끝난 상태에서 또 예외가 발생하면 프로그램은 terminate됨.

class Session {
public:
    Session();
    ~Session();
    ...
private:
    static void logCreateion(Session* objAddr);
    static void logDestruction(Session* objAddr);
};

-----------------------
Session::~Session() {
    logDestuction(this);
}

이렇게 구현하면 어떻게 될까?
logDestruction에서 예외가 발생하면 이 예외가 Session에서 catch 되지 않기 때문에, exception propagation이 발생할 것이다.
하지만 이전에 예외가 발생한 상태에서 이 소멸자가 불린다면?? terminate될 것이다.

따라서 terminate되지 않도록 코딩해야함.
어떻게? catch하면된다.

Session::~Session() {
    try {
        logDestruction(this);
    }  catch(...) {
        cerr << "Unable to log destruction of session obj.";
    }
}

하지만 이렇게 짜면 catch 블록안에 있는 operatior<< 에서 예외가 발생할 가능성이 남아있음.

Session::~Session() {
    try {
        logDestruction(this);
    } catch(...) { }
}

그럼 이렇게 catch에서 아무것도 안하면 되나? OK;;
"예외가 소멸자를 빠져나가지 못하게 한다" 라는 목적을 달성한 것이기 때문.

예외가 소멸자를 빠져나가게 하면 안되는 이유가 하나 더 있다.
예외가 소멸자를 빠져나가면 소멸자는 실행이 제대로 끝나지 않은 상태로 남게 됨.

예를들어 Session이 데이터베이스 트랜잭션을 시작하고, 소멸자에서 그 트랜잭션을 끝내도록 구현되어 있다면?

Session::Session() {
    logCreateion(this);
    startTransaction();
}

Session::~Session() {
    logDestuction(this);
    endTransaction();
}

logDestruction에서 예외가 발생하면 트랜잭션이 끝나지 않은 채로 남게된다.
logDestruction과 endTransaction의 순서를 바꾸면 되지않을까? 하는 생각은 하지 않으셨으면 좋겠습니다. endTransaction도 예외를 일으킬 수 있잖아요? (??)
결국 답은 try-catch 밖에 없다.
Posted by outshine90
,