MoreEffectiveC++

1. Exception

1.1. Item 9: Use destuctors to prevent resource leaks.

  • Item 9: 자원이 새는걸 막는 파괴자를 사용해라.

귀여븐 동물들 클래스를 만들어 보자.

ALA는 (Adorable Little Animal이다.)
~cpp 
    class ALA{
    publid:
        virtual void processAdiption() = 0;
        ...
    };
    class Puppy: public ALA{
    publid:
        virtual void processAdiption();
        ...
    };
    class Kitten: pubic ALA{
    publid:
        virtual void processAdiption();
        ...
    };

일단 여러분은 파일에서 부터 puppy와 kitten와 키튼의 정보를 이렇게 읽고 만든다. 사실 Item 25에 언급할 virtual constructor가 제격이지만, 일단 우리에 목적에 맞는 함수로 대체한다.
~cpp 
    ALA * readALA(istream& s);

다음과 같은 느낌(?)으로 출력한다.
~cpp 
    void processAdoptions( istream& dataSource)
    {
        while (dataSource) {
            ALA *pa = readALA(dataSource);
            pa->processAdoption();
            delete pa;
        }
    }
pa에 해당하는 processAdoption()은 오류시에 exception을 던진다. 하지만, exception시에 해당 코드가 멈춘다면 "delete pa;"가 수행되지 않아서 결국 자원이 새는 효과가 있겠지 그렇다면 다음과 같이 일단 예외 처리를 한다. 물론 해당 함수에서 propagate해주어 함수 자체에서도 예외를 발생 시킨다.

~cpp 
    void processAdoptions( istream& dataSource)
    {
        while (dataSource) {
            ALA *pa = readALA(dataSource);
            try{
                pa->processAdoption();
            }
            catch ( ... ) {
                delete pa;
                throw;
            }
            delete pa;
        }
    }
방법은 올바르다. 예외시에 해당 객체를 지워 버리는것, 그리고 이건 우리가 배운 try-catch-throw를 충실히 사용한 것이다. 하지만.. 복잡하지 않은가? 해당 코드는 말그대로 펼쳐진다.(영서의 표현) 그리고 코드의 가독성도 떨어지며, 차후 관리 차원에서 추가 코드의 발생시에도 어느 영역에 보강할것 인가에 관하여 문제시 된다.

여기에서 재미있는 기법을 이야기 해본다. 차차 소개될 smart pointer와 더불어 Standard C++ 라이브러리에 포함되어 있는 auto_ptr template 클래스를 이용한 해결책인데 auto_prt은 이렇게 생겼다.

~cpp 
    template<class T>
    class auto_ptr{
    public:
        auto_ptr(T *p = 0) : ptr(p) {}
    private:
        T *ptr;
    };

그리고 해당 auto_prt의 적용한 모습은 다음과 같이 보인다.
~cpp 
    void processAdoptions(istream& dataSource)
    {
        while (dataSource){
            auto_ptr<ALA> pa(readALA(dataSource));
            pa->processAdoption();
        }
    }

예외 발생시에 함수가 종료되면 auto_ptr상의 객체는 무조건 해제된다.

자자 다음 코드도 자원이 셀것이다.
~cpp 
    void displayIntoInfo(const Information& info)
    {
        WINDOW_HANDLE w(createWindow());
        display info in window corresponding to w;
        destroyWindow(w);
    }

일반적으로 C의 개념으로 짜여진 프로그램들은 createWindow and destroyWindow와 같이 관리한다. 그렇지만 이것 역시 destroyWindow(w)에 도달전에 예외 발생시 자원이 세는 경우가 생긴다. 그렇다면 다음과 같이 바꾸어서 해본다.

~cpp 
    class WidnowHandle{
    public:
        WindowHandle(WINDOW_HANDLE handle) : w(handle) {}
        ~WindowHandle() {destroyWindow(w); }
        
        operator WINDOW_HANDLE() {return w;}
    private:
        WINDOW_HANDLE w;
    
        WindowHandle(const WindowHandle&);
        WindowHandle&   operator=(const WindowHandle);

auto_ptr과 같은 원리이다.
~cpp 
    void displayIntoInfo(const Information& info)
    {
        WINDOW_HANDLE w(createWindow());
        display info in window corresponding to w;
    }

1.2. Item 10: Prevent resource leaks in constructors.

  • Item 10: 생성자에서 자원이 세는걸 막아라.

자 당신이 멀티미디어 주소록을 만든다고 상상하고, 프로그램을 짜보자 전화번호, 목소리, 사진, 이름 따위가 들어가야 할것이다. 다음 대강의 구현 코드들을 보면
~cpp 
    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 the Name;
        string the Address;
        list<phoneNumber> thePhones;
        Image *theImage;
        AudioClip *theAudioClip;
    };

각각의 BookEntry는 이름과 더불어 다른 필드를 가지고 있으며 기본 생성자는 다음과 같다.

~cpp 
    // 기본 생성자
    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 (audioClipFileName != "") {  // 소리 정보를 생성한다.
            theAudioCilp = new AudioClip( audioClipFileName);
        }
    }
    BookEntry::~BookEntry()
    {
        delete theImage;
        delete theAudioClip;
    }

생성자는 theImage와 theAudioClip를 null로 초기화 시킨다. C++상에서 null값이란 delete상에서의 안전을 보장한다. 하지만... 위의 코드중에 다음 코드에서 new로 생성할시에 예외가 발생된다면?
~cpp 
    if (audioClipFileName != "") {
        theAudioClip = new AudioClip(audioClipFileName);
    }

자자 예를들어서 이런 함수에서 예외 처리를 해야할것이다.
~cpp 
    void testBookEntryClass()
    {
        BookEntry b( "Addison-Wesley Publishing Company", "One Jacob Way, Reading, MA 018678");
        ...
    }

이렇게 try-catch-throw로 말이다.
~cpp 
    void testBookEntryClass()
    {
        BookEntry *pb = 0;
        try{
            pb = new BookEntry( "Addison-Wesley Publishing Company", "One Jacob Way, Reading, MA 018678");
        }
        catch ( ... ) {
            delete pb;
            throw;
        }
        delete pb;
    }

이렇게 해도 여전히 문제는 남는다. 무엇이냐 하면, 만약 BookEntry의 생성자중에서 AudioClip 객체 생성중에 예외를 propagate하면 바로 위 코드중 pb 포인터에 null을 반환해 버린다. 반납된 이렇게 되면 이미 정상적으로 생성된 theImage를 지우지 못하는 사태가 발생해 버리는 것이다.

그렇다면 생성자의 내부에서 다시 try-catch-throw로 해야 할것이다.

~cpp 
    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 != "") {
                theAudioCilp = new AudioClip( audioClipFileName);
            }
        }
        catch (...){
            delete theImage;
            delete theAudioClip;
            throw;
        }
    }

자 이렇게 해주면 문제 될것이 없다. 자 이상태에 refactoring이 필요한 코드들이 보일것이다 하겠다. delete부분을 함수로 역어 네는 것이다.

~cpp 
    class BookEntry{
    public:
        ...
    private:
        ...
        void cleanup(); // 이것이 객체를 삭제하는걸 묶은것
    };

~cpp 
    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();
    }

자 이제 깨끗이 해결 된 것으로 보인다. 하지만 이번에는 이런 경우를 상정해 보자

~cpp 
    class BookEntry{
    public :
        ...
    private:
        ...
        Image * const theImage;
        AudioClip * const theAudioClip;
    }

이런 const 포인터의 경우에는 반드시 초기화 리스트를 이용하여 인자를 초기화 해주어야 하는 경우이다.

~cpp 
    class BookEntry{
    public :
        ...
    private:
        ...
        Image * initImage(const string& imageFileName);
        AudioClip * initAudioClip(const string& theAudioClip);
    }

    BookEntry::BookEntry(const string& name,
                         const string& address,
                         const string& imageFileName,
                         const string& audioClipFileName)
    :theName(name), theAddress(address), 
     theImage(initImage(imageFileName)), theAudioClip(initAudioClip(AudioCilpFileName))
    {}
    // theImage는 가장 처음 초기화 되어서 자원이 세는 것에대한 걱정이 없다. 그래서 해당 소스에서 
    // 예외처리를 생략하였다.
   Image * initImage(const string& imageFileName)
    {
        if (imageFileName != "") return new Image(imageFileName);
        else return 0;
    }
    // theAudioClip는 두번째로 초기화 되기 때문에, 예외의 발생경우 이미 할당되어진 theImage의 자원을 
    //  해제해주는 예외 처리가 필요하다. 
    AudioClip * initAudioClip(const string& theAudioClip)
    {
        try{
            if (adioClipFileName != ""){
                return new AudioClip(audioClipFileName);
            }
            else return 0;
        }
        catch ( ... ){
            delete theImage;
            throw;
        }
    }
이런 해결 방법은 올바르다 하지만, 이전에 언급한 것과 같이 소스 사후 관리와 현재 소스 자체도 많이 지저분하다

그래서 더 좋은 방법은 Item 9에서 언급한 방법을 사용하는 것이다.
~cpp 
    class BookEntry{
    pubcli:
    ...
    private:
    ...
    const auto_ptr<Image> theImage;
    const auto_ptr<AudioClip> theAudioClip;
그리고 생성자의 코드를 이렇게 한다.
~cpp 
    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)
      {}

이렇게 디자인 할경우 파괴는 자동으로 이루어 진다. 그러므로 파괴자는
~cpp 
    BookEntry::~BookEntry()
    {}
이렇게 아무것도 해주지 않아도 객체가 같이 파괴되고 리소스가 새는 것을 방지 할수 있다.

1.3. Item 11: Prevent exception from leaving destuctors.

  • Item 11: 파괴자로 부터의 예외 처리를 막아라.

파괴자 호출은 두가지의 경우가 있다. 첫번째가 'normal'상태로 객체가 파괴되어 질때로 그것은 보통 명시적으로 delete가 불린다. 두번째는 예외가 전달되면서 스택이 풀릴때 예외 처리시(exception-handling) 객체가 파괴되어 지는 경우가 있다.

다음 예제는 online 컴퓨터 세션을 위한 Session 클래스를 생각해 본 것이다. 각 세션 객체들은 생성과 파되된 날짜를 기록해야만 한다.
~cpp 
    class Session{
    public:
        Session();
        ~Session();
        ...
    private:
        static void logCreation(Session *objAddr);
        static void logDestruction(Session *objAddr);
    };

다음과 같이 파괴자에서 로그 데이터를 남긴다.
~cpp 
    Session::~Session()
    {
        logDestruction(this);
    }

자 이건 괜찮아 보인다. 하지만 저 logDestruction상에서 예외가 발생한다면 어쩌게 할것인가? 해당 소스는 Session의 파괴자 안에서는 예외를 잡지 못한다. 그래서 해당 파괴자를 호출한 자에게 예외를 던진(전달한)다. 그렇지만 다른 에러들이 던져진 상황에서 파괴자가 스스로 자신을 부른거라면 함수의 종료가 자동으로 이루어지기를 원할 것이다. 그리고 당신의 프로그램은 이쯤에서 머추어 버릴 것이다. -해석이 이상하군, 암튼 다른 예외 처리시에 세션 파괴자 로그시 예외가 발생한다면 프로그램이 멈춘다는 소리다.

아마 대다수의 사람들이 이런 상태로 빠지는걸 원하지 않을 것이다. Session 객체의 파괴는 기록되지 않을 태니까. 그건 상당히 커다란 문제이다 그러나 그것이 좀더 심한 문제를 유발하는건 프로그램이 더 진할수 없을 때 일것이다. 그래서 Session의 파괴자에서의 예외 전달을 막아야 한다. 방법은 하나 try-catch로 잡아 버리는 것이다.
~cpp 
    Session::~Sessioni()
    {
        try{
            logDestruction(this);
        }
        catch ( ... ){
            cerr << "해당 주소에서 세션 객체의 파괴기록이 되지 않습니다."
                 << "-주소:"
                 << this << ".\n";
    }       
하지만 이것도 원래의 코드보다 안전할 것이 없다. 만약 operator<< 부를때 exception이 발생한다면 파괴자가 던지는 exception으로 다시 우리가 해결하고자 하는 원점으로 돌아가 버린다. 그렇다면
~cpp 
    Session::~Sessioni()
    {
        try{
            logDestruction(this);
        }
        catch ( ... ){ }       
이렇게 아무런 처리를 하지 않는다면 logDestuction에서 발생한 예외가 전달되는걸 막고 프로그램 중지를 위하여 스택이 풀려나가는걸 막을수는 있을 것이다.

하지만 이런 두번째의 생각도 파괴자에서 발생하는 모든 에러를 막아 버리고 그냥 넘어가 버린다는 단점이 있다.
밑의 코드는 DB의 트랜젝션을 이용해서 에러를 처리하는 모습이다.
~cpp 
    Session::Session()
    {
        logCreation(this);
        startTransaction();
    }
    Session::~Session()
    {
        logDestruction(this);
        endTransaction();
    }
이럴 경우에는 Session의 파괴자에게 문제를 제거하는 명령을 다시 내릴수 있따 하지만 endTransaction이 예외를 발생히킨다면 다시 try-catch문으로 돌아 갈수 밖에 없다.

난제다 난제

1.4. Item 12: Understand how throwing an exception differs from passing a parameter or calling a virtual function

  • Item 12: 가상 함수 부르기나, 인자 전달로 처리와 예외전달의 방법의 차이점을 이해하라.

다음의 가상함수의 선언과 같이 당신은 catch 구문에서도 비슷하게 인자들을 넣을수 있다.
~cpp 
    class Widget { ... };

    void f1(Widget w);
    void f2(Widget& w);
    void f3(const Widget w);
    void f4(Widget *pw);
    void f5(const Widget *pw);

    catch (Widget w) ...
    catch (Widget& w) ...
    catch (const Widget w) ...
    catch (Widget *pw) ...
    catch (const Widget *pw) ...

그래서 아마 함수호출에서 인자 전달과 과 예외가 전달되는 것이 기본적으로 같은것이라고 생각 할지도 모른다. 분명 둘은 비슷한 면이 있다. 하지만 중요한 차이점 역시 존재 한다.

자, 비슷한 면은 언급해보면, 함수 예외 모두 에 인자를 전달할때 세가지로 전달할수 있다. 값(by value)이냐 참조(by reference)냐, 혹은 포인터(by pointer)냐 바로 이것이다. 하지만 이 함수와 예외에서의 인자의 전달 방식은 구동 방법에서 결정적인 차이점을 보인다. 이런 차이점은 당신이 함수를 호출할때 최종적으로 반환되는 값(returns)이 해당 함수를 부르는 위치로 가지만, 예외의 경우에 throw의 위치와 return의 위치가 다르다는 점에서 기인한다.

다음 함수에서 Widget의 인자 전달과 예외에서의 전달을 생각해 보자.
~cpp 
    // 이 소스는 위의 Widget의 일환이라고 생각하면 무리 없겠다.
    istream operator>>(istream& s, Widget& w);
    void passAndThrowWidget()
    {
        Widget localWidget;
        cin >> localWidget;
        throw localWidget;
    }

localWidget이 operator>> 로 전달될때는 복사의 과정이 일어나지 않는다. 대신 operator>> 안의 참조 w가 localWidget과 묶여서 어떠한 과정을 처리하게 된다. 하지만 예외의 처리에서 localWidget은 좀 다른 이야기를 만들어 나간다. 예외가 값이나, 참조를 잡든 잡지(pointer는 잡지 못한다.) 않든 상관 없이 localWidget의 사본이 만들어지고, 그 사본은 catch로 저낟ㄹ 된다. 왜냐하면 passAndThrowWidget의 영역을 벗어나면 localWidget의 파괴자의 호출이 되기 때문에 반드시 이렇게 되어야 한다. 이것은 C++ 에서 예외는 항상 사본으로 전달된다는 이유가 된다.

이런 복사의 과정은 아무리 파괴의 위험이 없는 예외라도 이루어 진다. 예를 들자면

~cpp 
    void passAndThrowWidget()
    {
        static Widget localWidget;
        cin >> localWidget;
        throw localWidget;
    }

해당 사본은 구지 복사할 필요가 없을 것이다. 하지만 catch 는 복사해 나가고 그래야만 catch 에서 localWidget의 사본을 편집해서 이용할수 있다. 이러한 복사의 규칙은 함수 전달과 예외 인자 전달의 차이점을 설명해 준다.

객체가 예외를 위하여 복사가 될때 복사는 해당 객체의 복사생성자(copy constructor)에 의하여 수행 된다. 이 복사생성자는 객체의 dynamic형이 아닌 static 형에 해당하는 클래스중 하나이다. 개념의 확인을 위해 위 소스의 수정 버전을 생각해 보자
~cpp 
class Widget { ...}
class SpecialWidget: public Widget { ... };
void passAndThrowWidget()
{
    SpecialWidget localSpecialWidget;
    ...
    Widget& rw = localSpecialWidget;
    throw rw;                           // rw의 형 즉 Widget의 복사생성자가 작동하여 복사해 예외를 발생시킨다.
}

다음의 경우 passAndThrowWidget 이 던지는건 Widget 이다. 위에서 언급했듯이 static type으로 예외는 전달된다. 컴파일러는 rw가 SpecialWidget으로의 동작을 전혀 생각하지 않는다.

예외처리시에 다른 객체의 사본이 전달 된다는 점은 예외가 계속 전달(퍼져나갈때,propagate)에도 한가지의 고려사항이 발생한다. 다음의 두가지의 catch 블럭은 차이점이 있다. 하지만 외견상 같은 역할을 한다.
~cpp 
    catch (Widget& w)
    {
        ...
        throw;              // 해당 객체를 다시 복사하지 않고 던지며, 해당 예외를 propagete 한다. 
    }
    catch (Widget& w)
    {
        ...
        throw w;            // 해당 객체를 다시 복사해서 그 사본을 propagate한다.
    }
주석에 되어 있는데로, 생각해 보라. throw가 복사생성자를 호출하지 않아서 효율적이다. 그리고 throw는 어떠한 형이든 예외를 전달한는데 상관하지 않는다. 하지만, 사실 예외자체가 그 형에 맞게 던져지므로 걱정이 없다. 하지만 catch문에서 예외를 던지는 객체의 형태를 바꿀 필요성이 있을때 후자를 사용해야 겠다.

자 그럼 다음의 세가지 catch에 관해서 시험해 보자. passAndThrowWidget에서 발생한 예외는 다음의 세가지의 경우로 잡을수 있는걸 예상할수 있다.
~cpp 
catch (Widget w) ...        // 값으로 전달

catch (Widget& w) ...       // 참조 전달

catch (const Widget& w) ... // 상수 참조로 전달
전달된 객체는 간단히 참조로 잡을수 있다;그것은 상수 참조로 전달될 필요성은 없다. 그러나 상수 참조가 아닌 전달 임시 객체들은 함수를 부르는걸 허용하지 않는다.

그럼 이들의 차이점을 살펴보고 예외 객체들을 리턴해 보자.

~cpp 
    catch (Widget w) ...    // 값으로 전달
이렇게 값으로 전달하면 두번의 복사가 일어나는걸 예상할수 있다. throw시 한번 catch시 한번 ok? 비효율이다.

참조로 전달할때 예상해 보자
~cpp 
    catch (Widget& w) ...           // 참조 전달.
    catch (const Widget& w) ...     // 상수-참조 전달
우리는 지금까지 복사에 의한 지불을 생각할수 있는데 참조의 전달은 반대로 복사하는 작업이 없다. 즉, 한번의 복사 이후 계속 같은 객체를 사용하게 되는 셈이다.

우린 아직 포인터 전달에 의한 걸 의논하지 않았다. 하지만 포인터를 이용해 예외를 던지(전달:throw)하는 것은 함수상에서 포인터를 전달(pass)하는 것과는 다른걸 알수 잇을 것이다. 즉, 포인터의 복사본이 이동하는데, 이렇게 되면 pointer를 전달하는 쪽의 영역에서 throw에 의해 튀어 나가면 포인터가 가리키고 있는 객체는 소멸되므로 포인터에 의한 예외 전달(던지는것:throw)는 피해야 한다. (전역의 static 객체의 포인터라면 이야기는 달라진다. 뒤에 다룬다.)

자 그럼 예외를 던질때의 형에 관한 주의를 살펴 보자. C++의 암시적 변환에 의한 것이 그 문제의 발단인데, 코드를 보자 표준 수학 라이브러리에서
~cpp 
    double sqrt(double);
를 이렇게 사용 할수 있다.
~cpp 
    int i;
    double sqrtOfi = sqrt(i);
Item 5에도 언급되어 있듯이 C++상에서의 암시적 변환은 광 범위하다. i형의 double변환은 가뿐? 하다. 하지만 다음을 보자
~cpp 
    void f (int value)
    {
        try {
            if (someFunction() ) {      // someFunction()이 참이면 int형의 변수를 예외로 던진다.
                throw value;
            }
            ...
        }
        catch (double d) {      // 해당 핸들은 double일때만 예외를 잡을수 있다. 그럼 value를 던지는
            ...                 // try에서의 예외는 절대로 잡히지 않는다. 의도한 바가 아니리라.
        }
        ...
    }
이런 사항을 유의 해야 한다. 예외에서는 암시적 변환을 생각하지 않는다.

그럼 예외의 변환에는 크게 두가지의 생각할 점이 있는데. 첫번째가 상속 관계(예외 상의) 이다. 예외에서는 한 예외 객체에서 파생된 다른 예외객체들을 잡는것이 가능한데 예를들어서 표준 C++ 라이브러리에서의 예외 상속도는 이렇게 구성되었다. (모든 예외가 나왔는지는 모르겠다.)
~cpp 
    exception 
        |
        +-logic_error
        |   |
        |   +-domain_error
        |   +-invalid_argument
        |   +-length_error
        |
        +-runtime_error
            |
            +-range_error
            +-underflow_error
            +-overflow_error

catch에서 부모 객체로 잡으면 자식 객체의 예외들이 다 잡히는 식이다 .예를 들자면
~cpp 
catch (runtime_error) ...           // 이렇게 선언하면 runtime_error자식인
catch (runtime_error&) ...          // range_error, underflow_error
catch (const runtime_error&) ...    // overflow_error이 다 잡히는 거다.


catch (runtime_error*) ...          // 이렇게 하면 runtime_error*
catch (const runtime_error*) ...    // range_error*, underflow_error*, overflow_error* 으로 잡히는 것이다.

두번째의 생각할 점은 모든 예외를 잡고 싶으면
~cpp 
    catch (const void*) ...
이렇게 하면 어떠한 포인터 type라도 잡을 것이다.

마지막으로 인자 넘기기와 예외 전달(던지기:throw)의 다른 점은 catch 구문은 항상 catch가 쓰여진 순서대로 (in the order of their appearance) 구동된다는 점이다. (영어 구문을 참조하시길) 말이 이상하다. 그냥 다음 예제를 보자

~cpp 
    try{
        ...
    }
    catch (logic_error& ex) {       // 여기에서 모든 logic에러가 잡힌다.
        ...
    }
    catch (invalid_argument & ex){  // 이 문은 작동을 하지 않는다. 위의 catch구분에서 이미 잡아 버린다.
        ...
    }

반대로 가상함수를 부를때 일어나는일이 있다. 당신이 가상함수를 호출하면 함수는 해당 객체의 가장 합당한 함수를 dynamic으로 찾아낸다. 이것은 최고로 적합한 것(best fit)을 의미하지 가장 처음에 찾아 지는 것(first fit)을 의미하는 것이 아니다. 위의 소스를 반대로 한다면

~cpp 
    try{
        ...
    }
    catch (invalid_argument & ex){  // invalid_argument 예외는 이곳에서 잡힌다.
        ...
    }
    catch (logic_error& ex) {       // 여기서 모든 다른 logic_error 관련은 이곳에서 잡힌다.
        ...
    }
이렇게 돌아가는 거다.

자자 정리
  • 첫째로 예외 객체는 항상 복사 된다.
  • 둘째로 던저지는 객체는 함수로 전달될때 비하여 형에 대한 변환이 형에 영향 받기 쉽다.
    예외 객체는 상속에 규칙을 따른다. (설명을 보시길)
  • 셋째로 소스 코드에 나타나는 순서대로 예외는 잡힌다.

1.5. Item 13: Catch exception by reference

  • Item 13: 예외는 참조(reference)로 잡아라.

catch 구문을 사용할때 해당 구문을 통해서 전달받은 예외 객체들을 받는 방법을 잘알아야 한다. 당신은 세가지의 선택을 할수 있다. 바로 전 Item 12에서 언급한 것처럼 값(by value), 참조(by reference), 포인터(by pointer)이렇게 세가지 정도가 될것이다.

자, 먼저 pointer(by pointer)에 관한 전달을 생각해 보자. 이론적으로 이 방법은 throw위치에서 catch구분으로 예외를 특별한 변화 없이 느린 프로그램 수행 상태에서 전달하기에 가장 좋은 방법이다. 그 이유는 포인터의 전달은 해당 예외 객체가 복사되는 일없이 포인터 값만 전달되는 방법만을 취해야 하기 때문이다. 말이 좀 이상한데 예외를 보면서 설명한다.

Item 12에서 언급한것과 같이 예외는 복사되어서 전달된다. 그걸 생각해라.
~cpp 
    class exception { ... };

    void someFunction()
    {
        static exception ex;        // 이렇게 메모리에 항상 존재하는 객체만을 전달할수 있다.
        ...
        throw &ex;    // 포인터로 전달, 해당 함수 영역을 벗어나므로, static만이 살아 남을수 있다.
        ...
    }
    void doSomething()
    {
        try{
            someFunction();         // 여기에서 exception *을 던진다.
        }
        catch ( exception *ex) {    // exception* 을 잡고 아무런 객체도 복사되지 않는다.
            ...
        }
    }

이 코드는 깨끗하게 보이지만, 최선책은 아니다. 이런 일을 위해서 프로그래머는 예외 객체를 항상 품고있는 프로그램을 작성해야 할것이다. 간단히 전역(Global) staitc으로 선언하면 된다고 반문하겠지만, 전역의 위험성은 프로그래머가 그걸 쉽게 까먹을수 있다는데 있다. 다음 예제를 보면
~cpp 
void someFunction()
{
    exception ex;       // local 예외 객체인데 이 함수의 영역을 벗어나면 파괴되어 진다.

    ...
    throw &ex;          // 그럼 이건 말짱 헛일이라는 소리 이미 파괴된 객체를 가리키고 있으니
    ...
}
자 이건 나쁜 코드의 유형일 것이다. 주석에서 언급한것과 같이 함수에서 벗어나면 new나 static이 아닌이상 만들어진 객체는 파괴되어 진다. 그리고 catch에서는 파괴되어진 객체의 주소 값을 받게 되는 것이다.

해당 코드를 다음과 같이 new heap object로 대체할수 있을 것이다.
~cpp 
    void someFunction() 
    {
        ...
        throw new exception;    // 이것도 어폐가 있는게, new에서 예외 발생하면 어떻게 할것가?
        ...
    }

이것도 피해야 할 방법이다. 왜냐하면 I-just-caught-a-pointer-to-a-destoyed-object 문제 때문이다. 게다가 catch구문에서 직면한 또하나의 문제는 대체 이 포인터를 누가 어디서 지우느냐 이다. 다른 면으로 생각해볼 문제는 예외 객체가 heap상에 배치된다면 지워 지지 않은 예외 객체는 틀임없이 resource leak를 발생 시킬 것이다. 너무 뻔한 이야기 인가. 그리고 프로그램의 행보가 어떻게 될지 예측 할수도 없다. 안그런가?

몇몇 클라이언트는 전역(global)이나 정적 객체를(static object)의 주소를 넘기자고 말하고, 몇몇은 heap상의 예외 객체의 주소를 전달하자고 말한다. 이처럼 포인터를 통한 예외의 전달은 (Catch by pointer) 아리송한 문제를 발생 시킨다. 지워 졌는가? 안지워 졌는가? 항상 대답은 확실하지 않다.

게다가 catch-by-pointer(포인터를 통한 예외 전달)은 언어상에서 사람들의 대립을 유도 한다. 네가지의 표준 예외 객체들들( bad_alloc(Item 8:operator new에서 불충분한 메모리 반환), bad_cast(Item 2:dynamic_cast에서 참조 실패), bad_typeid(dynamic_cast일때 null포인터로 변환), bad_exception(Item 14:예측 못하는 예외로 빠지는 것 unexpected exception 문제) 가 예외 객체들의 모든 것인데, 이들을 향한 기본적인 포인터는 존재하지 않는다. 그래서 당신은 예외를 값으로(by value)혹은 참조로(by reference) 밖에는 대안이 없다.

Catch-by-value는 표준 예외 객체들 상에에서 예외 객체의 삭제 문제에 관해서 고민할 필요가 없다. 하지만 예외가 전달될때 두번의 복사가 이루어 진다는게 문제다. (Item 12참고) 게다가 값으로의 전달은 slicing problem이라는 문제를 발생시킨다. 이게 뭐냐 하면, 만약 표준 예외 객체에서 유도(상속)해서 만들어진 예외 객체들이 해당 객체의 부모로 던저 진다면, 부모 파트 부분만 값으로 두번째 복사시에 복사되어서 전달되어 버린다는 문제다. 즉 잘라버리는 문제 "slice off" 라는 표현이 들어 갈만 하겠지. 그들의 data member는 아마 부족함이 생겨 버릴 것이고 해당 객체상에서 가상 함수를 부를때 역시 문제가 발생해 버릴 것이다. 아마 무조건 부모 객체의 가상 함수를 부르게 될 것이다.(이 같은 문제는 함수에 객체를 값으로 넘길때도 똑같이 제기 된다.) 예를 들어서 다음을 생각해 보자
~cpp 
    class exception {
    public:
        virtual const char * what() throw();
        ...
    }

    class runtime_error:public exception{ ... };

    class Validation_error : public runtime_error{      // 자 해당 객체는 runtime_error를 상속해서 만들었고
    public:             
        virtual const char * what() throw();            // 이 가상함수는 exception상에 있는 것이다.
        ...
    }
    void someFunction()
    {
        ...
        if ( a validation 테스트 실패){
            throw Validation_error();
        }
        ...
    }
    void doSomething()
    {
        try{
            someFunction();
        }
        catch (exception ex) {      // item 12에서 언급했듯 exception의 자식 예외객체들은 다 잡힌다.
            cerr << ex.what();      // 값으로 부모만 복사했기 때문에 what() 가상함수는 exception상의 
                                    // 가상 함수가 불린다. 개발자의 의도는 Validation_error 상의 가상함수를
                                    // 부르길 원하는 것 이었다.
            ...
        }
    }
주석에 언급되어 있듯이 이 버전은 slicing 문제가 발생한다. 구차한 설명 귀찮다. 결론은 값으로(by value)의 예외 객체 전달은 이런 slicing 문제로 당신이 원하는 행동을 절대로 못한다.

자자 그럼 남은건 오직 catch-by-reference(참조로서 예외 전달)이다. catch-by-reference는 이제까지의 논의를 깨끗이 없애 준다. catch-by-pointer의 객체 삭제 문제와 표준 예외 타입들을 잡는거에 대한 어려움, catch-by-value와 같은 slicing 문제나 두번 복제되는 어려움이 없다. 참조로서 예외 전달에서 예외 객체는 오직 한번 복사되어 질 뿐이다.

다음 예제를 보자.
~cpp 
void someFunction()
{
    ...
    if ( validation 테스트 에러 ){
        throw Validataion_error();
    }
    ...
}

void doSomething()
{
    try{
        someFunction();
    }
    catch (exception& ex){      // 이부분을 참조로만 바꾸었다. 이전의 예제와 특별히 바뀐게 없다.
                                // 하지만 이부분이 바뀌어서
        cerr << ex.what();      // 여기서의 가상 함수도 Validation_error의 메소드가 불린다.
        ...
    }
}

해당 소스는 catch에서 참조로만이 바뀌었다. &하나만이 추가되어 지금까지 제기된 문제가 사라져 버린다.

catch-by-reference는 이제까지의 문제에 모든 해결책을 제시한다. (slicing, delete문제 etc)그럼 이제 결론은 하나 아닐까?

Catch exception by reference!

1.6. Item 14: Use exception specifications judiciously.

  • Item 14: 예외를 신중하게 사용하라.
(judiciously- 신중한)

일단 이 주제를 부정하는 이는 없으리라.:예외는 적절한 곳에 표현되어야 한다. 그들은 코드를 더 이해가기 편하게 만들어 준다. 왜냐하면 아마 명시적으로 표현된 예외 상태가 전달(던저:throw-이하 던진다는 표현으로) 될 것이기 때문이다. 그렇지만 예외는 주석(comment)보다는 모호하다. 컴파일러는 때때로 컴파일중에 정확히 일치하지 않은 예외들을 발견할수도 있으며, 만약 함수가 예외 스펙(명세:이하명세)상에 제대로 명기되지 않은 예외를 전달(던졌)다면 잘못은 실행시간(runtime)에 발견된다. 그리고 특별한 함수인 unexpected는 자동으로 불리게 된다. 이렇든 예외처리는 상당히 매력적인 면을 가지고 있다.

  • 하지만 보통 아름다움은 표면이 아닌 내면에 존재한다.
unexpected에 관련한 기본적인 행동은 terminate를 호출해서 terminate내에서 abort를 호출로 강제로 프그램을 멈추게 한다. 이 의미는 바로 abort는 프로그램을 종료할때 깨끗이 지우는 과정을 생략하기 때문에 활성화된 스택 프레임내의 지역 변수는 파괴되지 않는다.(즉, 프로그램이 멈추고 디버그시 그 상황에 현재의 자료 값을 조사할수 있다는 의미). 그래서 예외 처리의 명세을 어긴 문제는 상당히 심각한 상황이나, 거의 발생하지 않은 상황이다. 불행히도 그런 심각한 상황을 이르게 하는 함수 작성이 용이하다는게 문제이다. 컴파일러는 오직 예외 명세에 입각한대로 부분적으로 예외 사용에 관한 검사를 한다. 예외가 잡을수 없는것-언어 표준 상에서 거부하는(비록 주의(wanning)일지라도) 금지하는 것- 은 함수를 호출할때 예외 명세에서 벗어나는 함수일것이다.

다음의 f1함수에 같이 아무런 예외를 발생 안시키는 함수에 관해서 생각해 보자. 저런 함수는 아마 어떠한 예외라도 발생시킬수 있을 것이다.
~cpp 
    extern void f1();
자 그럼 예외 명세이 적용된 f2를 보자. 다음은 오직 int만을 예외로 던질것이다.
~cpp 
    void f2() throw(int);
f1이 f2의 함수 명세과 다른 예외를 던지더라도, C++상에서는 f2에서 f1를 부르는것을 허용한다.
~cpp 
    void f2() throw(int)
    {
        ...
        f1();
        ...
    }
이런 유연한 경우는 만약 예외 명세에 관한 새로운 코드가 과거의 예외 명세가 부족한 코드와 잘 결합할수 있음을 보인다.

당신의 컴파일러가 예외 처리규정에 만족하지 않은 루틴을 가진 함수의 코드를 호출하는데 별 무리없다고, 그러한 호출이 아마 당신의 프로그램에서 프로그램의 중지를 유도하기 때문에 당신은 소프트웨어를 만들때 최대한 그런 만족되지 않은 호출을 최소화 하도록 결과를 유도해야 할것이다. 시작시 가장 좋은 방향은 템플릿상에서의 예외 스펙를 최대한 피하는 것이다. 자 다음의 어떠한 예외도 던지지 않은 템플릿을 생각해 보자.
~cpp 
    template<class T>
    bool operator==(const T& lhs, const T& rhs) throw()
    {
        return &lhs == &rhs;
    }
이 템플릿은 oprator== 함수를 모든 형에 적용시키는 것이다. 아마 같은 주소에 같은 타입이면 true를 반환하지만 아니라면 그것은 false를 반환한다. 이런 템플릿은 아무런 예외도 던지지 않은 템플릿으로 부터 함수가 만들어지는 상태에 따라 적합한 예외가 포함된다. 하지만 그것은 꼭 사실이 아니다. 왜냐하면 operator&(주소 반환 operator)가가 꼭 같은 몇몇의 형들을 위해서 overload되었기 때문이다. 만약 사실이 그러하다면 operaotr&가 operator== 안쪽에서 불릴때 예외를 던질 것이다. 그렇게 되면 우리의 예외 명세는 거부되고, 곧장 unexpected 로 직진하게 되는거다.

이러한 특별난 예제는 더 일반적인 문제로, 다시 말하자면 템플릿의 형 인자로 전달되는 예외에 관한 정보를 알아낼 길이 없다는 점도 한몫이다. 우리는 거의 템플릿을 위한 의미있는 예외 명세를 제공할수 없다는 이야기다. 왜냐하면 템플릿은 거의 변함없이 그들이 형 인자를 몇가지의 방식으로만 쓰기 때문이다. 결론은? 템플릿과 예외는 어울리지 않는다.!

  • 두번째로 당신은 unexpected호출을 막기위하여 부족한 예외 명세의 규정으로 인하여 불리는 함수상에서 예외 명세를 생략할수 있다.
이것은 간단하고 일반적인 생각이지만, 잃어버리기 쉬운 경우이다. 다음의 callback 함수 등록에 관한 예제를 보자
~cpp 
typedef void (*CallBackPtr) (int eventXLocation, int event YLocation, void *dataToPassBack);

class CallBack{
public:
    CallBack(CallBackPtr fPtr, void *dataToPassBack) :func(fPtr), data(dataToPassBack){}

    void makeCallBack(int eventXLocation, int eventYLocation) const throw();
private:
    CallBackPtr func;
    void *data
}

    void CallBack::makeCallBack(int eventXLocation, int eventYLocation)
    {
        func(eventXLocation, eventYLocation);
    }
이 코드에서 makeCallBack에서 func을 호출하는것은 func이 던지는 예외에 것을 알길이 없어서 예외 명세에 위반하는 위험한 상황으로 갈수 있다.

이런 문제는 다음과 같이 CallBackPtr상의 예외 명세를 좀더 구체화 시켜서 제거할수 있다.
~cpp 
typedef void (*CallBackPtr) (int eventXLocation, int event YLocation, void *dataToPassBack) throw();
다음과 같이 형이 선언되었으면 callback함수 등록시 아무것도 던지지 않는다는 조건이라면 예외를 발생할것이다.
~cpp 
    // 예외 명세가 없는 함수
    void callBackFcn1(int eventXLocation, int event YLocation, void *dataToPassBack);
    void *vallBackData;
    ...
    CallBack c1(callBackFcn1, callBackData); // 에러다 callBackFcn1은 여기에서 형이 맞지 않아. 에러를 던질것이다.

    // 예외 명세가 있는 함수
    void callBackFcn2(int eventXLocation, int event YLocation, void *dataToPassBack); throw()
    CallBack c1(callBackFcn2, callBackData); // 보다시피 알맞는 형이다.

이러한 함수 포인터 전달시 관련은 최근에 추가된거니 만약 컴파일러가 지원 못한다고 해도 놀랄것은 없다. (이책은 1996년에 나왔다. 하지만 지금도(2001년정도) 제대로 지원하는 컴파일러가 많지 않은걸로 안다.) 만약 컴파일러가 처리 못한다면 이런 실수의 방지는 당신 자신에게 달렸다.

  • 세번째로 당신은 "the system"이 아마 던지는 예외를 핸들링해서 unexcepted의 호출을 피할수 있다. 이러한 예외는 많은 부분이 new와 new[]시 메모리 할당 예외에서 bad_alloc이 발생하여 발생한다. 만약 당신이 new를 어떤 함수에서 쓴다면 우연이라도 bad_alloc 예외를 만날수 있는 가능성을 내포하는 셈이다.

자, 지금 1온스의 예방는 차후 1파운드의 피해보다 낳지만 때로는 예방이 어렵고 피해가 더 쉬운 경우도 있으리라. 언급한 것처럼 때때로 unexpected 예외 직접 맞서는 것은 처음에 그것을 에방하는것 보다 쉽다. 예를들자면 만약 당신이 예외명세를 엄격하게 작성했지만 당신은 예외 명세가 되어 있지 않은 라이브러리의 함수들을 강제로 부를수 있다. 함수상에서 코드들이 바뀌는 정이라서 unexpected예외를 막는것은 비실용적이다.

만약 unexpected예외를 막는것이 실용적이지 못하다면 당신은 C++가 unexpected를 다른 형식으로 바꾸어 버리는 기능을 이용해서 그러한 비실용적인 상태를 만회할수 있다. 다음 예는 unexpect와 같은 예외를 UnexpectedException 객체로 바꾼것을 생각해 본다.
~cpp 
class UnexpectedException {};

void convertUnexpected()
{
    throw UnexpectedException();
}

그리고 unexpected 함수를 convertUnexpected로 교체한다.
~cpp 
    set_unexpected(convertUnexpected);
이렇게 하면 unexpected예외는 convertUnexpected를 호출한다. 즉, 새로운 UnexpectedException 객체로 예외가 교체되었다. 하지만 제공되는 예외 명세에서 unexpected를 방지할려면 UnexpectedException 예외를 포함해야 한다. 예외를 객체로 던졌기에.. (만약 예외 명세에 UnexpectedException을 넣지 않았다면 unexpected가 교체되지 않은 것처럼 terminate가 불릴것이다.)

또 다른 방법은 unexpected 예외를 그냥 unexpected의 역할을 현재의 예외를 계속 던지기(rethrow)형태로 바꾸어 버리는 것이다. 이렇게 교체하면 예외는 아마 새로운 표준의 bad_exception 을 던지는 형태로 바뀐다. (정규 C++라이브러리에 포함)
~cpp 
    void convertUnexpected()
    {
        throw()     // 이건 현제의 예외를 계속 던진다는 의미
    }

    set_unexpected(convertUnexpected);
만약 위와 같이 하고 bad_exception(표준 라이브러리 상의 exception의 기본 예외 클래스)를 당신의 모든 예외 명세에 포함시키면 당신은 결코 당신ㄴ의 프로그램이 불시에 멈추어 버리는것에 대한 걱정을 할 요는 없을 것이다. 거기다가 규정에 맞지않는 예외들도 bad_exception으로 교체되고 예외는 기본 예외 대신에 다시 던저 퍼진다.(propagate)

--
이제 당신은 예외 명세가 많은 문제를 가지고 있을수 있음을 이해 할것이다. 컴파일러는 그들의 부분적인 쓰임새를 검사해서 템플릿에서 문제를 발생할 소지를 않으며, 컴파일러는 의외로 규칙위반을 하기 쉽고, 컴파일러가 제대로 되지 않으면 프로그램을 불시에 멈추어 지도록 유도할것이다. 예외 명세 역시 또다른 문제를 안고 있는데, 예외명세는 높은 수준의 호출자가 예외 발생을 대비할때도 unexpected로의 결과물을 만들어 낸다.

이야기를 위해 Item 11의 예제를 그대로 보자
~cpp 
    class Session{
    public:
        ~Session();
        ...
    private:
        static void logDestuction(Session *objAddr) throw();
    };

    Session::~Session()
    {
        try{
            logDestruction(this);
        }
        catch ( ... ) {}
    }
Session의 파괴자는 logDestruction을 호출한다. 하지만 명시작은 어떠한 예외도 해당 logDestruction에서 던지지 못하도록 막아놓았다. 한번 logDestuction이 실패할때 불리는 함수들에 대하여 생각해 보자. 이것은 아마 일어나지 않을 것이다. 우리가 생각한대로이건 상당히 예외 명세의 규정 위반으로 인도하는 코드이다. 이런 예측할수 없는 예외가 logDestruction으로 부터 퍼질때 unexpected가 풀릴 것이다. 기본적으로 그것은 프로그램을 멈춘다. 이 예제는 그것의 수정 버전이지만, 그런 수행을 Session 파괴자의 제작자가 원할까? 작성자는 모든 가능한 예외 를 잡으려고 노력한다. 그래서 그건 Session 파괴자의 catch블럭에서수행되는 것이 다다면 그건 불공평한 처사라고 보인다. 만약 logDestruction이 아무런 예외 명세를 하지 않는다면, I'm-willing-to-catch-it-if-you'll-just-give-me-a-chance 시나리오는 결코 일어나지 않을것이다. (이런 문제의 예방으로 unexpected의 교체에 대한 설명을 위해 언급해 두었다.)

예외 명세의 균형있는 시각은 중요한것이다. 그것은 예외 발생을 예상하는 함수들의 예외 종류들을 보면 훌륭한 문서화가 될것이고, 잘못된 예외 명세의 상황하의 프로그램은 기본적으로 주어지는 상태 즉, 즉시 멈추는 것을 정당화할 만큼 잘못된 일이다. 같은 시각으로 예외는 컴파일러에 의하여 오직 부분적인 점검만을 당하고 예외는 의도하지 않은 잘못을 양산하기 쉬울것이다. 게다가 예외는 unexpected 예외에서 발생하는 높은 레벨의 예외 잡는 문제에 대하여 예방할수 있다.

자 이런것들이 예외 명세를 현명하게 사용하는데 일조할 것이다. 당신의 함수에 예외를 더하기 전에 이런 사항에 대하여 한번쯤 생각해 보자.

1.7. Item 15: Understand the costs of exception handling

  • Item 15: 예외 핸들링에 대한 비용 지불 대한 이해

실행시간에 예외 핸들링을 위하여 프로그래머는 한쌍의 코드를 입력해야 한다. 예외 중에 각자의 포인트에 프로그래머는 각 try블럭에 들어가는 부분과 나오는 부분따위의 예외가 던저질때 객체 파괴의 필요성을 확인해야만 한다. 그리고 각 try 블록에서 프로그래머는 catch구문의 연계와 그들과 관계되어 있는 예외 객체의 종류에 대하여도 생각해 주어야 한다. 이런 것들은 결코 공짜가 아니다. 실행시간동안에 예외 명세에 대한 확인 작업도 그러며, catch구문에 객체가 던져 짔을때 객체의 파괴 부분에 대한 일도 역시 확장된다. 하지만, 만약 당신이 try, throw, catch키워드를 사용하지 않는다면 예외 핸들링에해단 비용은 발생하지 않고, 해당 키워드들에 대한 비용 지불도 미미한 양이다.

자 그럼 전혀 예외 핸들링을 하지 않았을때의 지불 비용을 생각해 보자, 당신은 객체들이 적재되고, 유지되는 트랙이 필요한 데이터 구조의 사용을 위해 공간에 대한 비용 지불을 한다. 그리고 당신은 이런 데이터 구조들을 갱신하고 유지하는데 필요한 시간에 대한 비용을 지불한다. 이런 비용들은 일반적으로 정당한 요구이다. 반면에 프로그램이 예외를 위한 지원이 없이 컴파일 된다면 예외 지원을 하고 컴파일 하는 반대의 경우보다 좀더 빠르고, 좀더 작은 용량을 차지한다.

이론적으로 당신은 이런(예외) 비용의 지출(선택,select)이 없어야 한다.:C++의 한 부분인 예외, 컴파일러는 예외를 지원해야한다.

프로그램은 일반적으로 독립적으로 object 파일들이 생성되어 지고, 단지 하나의 작성되어 만들어진 object파일에서 예외 처리가 없다면 다른 것들상의 예외 처리가 아무런 의미가 없기때문에, 당신이 예외처리코드를 사용하지 않는다면, 당신은 컴파일러 제작사들이 이런 예외들을 지원시 일어나는 비용을 없앨 것이라고 예상한다. 게다가 object파일이 예외를 빼기위해 아무런 상호간의 링크가 되지 않는다면 예외 처리가 들어간 라이브러리와의 링크는 어떨까? 즉, 프로그램의 어떤 부분이라도 예외를 사용한다면 나머지 프로그램의 부분들도 예외를 지원해야 한다. 이런 부분적 예외 처리 상황은 실행시간에 정확한 예외를 잡는 수행이 불가능 하게 만들것이다.

물론 저것은 이론이다. 실질적으로 예외 지원 밴더들은 당신이 예외 작성을 위한 코드의 첨가를 당신이 예외를 지원하느냐 마느냐에 따라 조정할수 있도록 만들어 놓았다.(작성자주:즉 예외 관련 처리의 on, off가 가능하다.) 만약 당신이 당신의 프로그램의 어떠한 영역과, 연계되는 모든 라이브러리에서 try, throw, catch를 빼고 예외 지원 사항을 빼고 당신 스스로 속도, 크기 같은 예외처리시 발생하는 단점을 제거할수 있을 것이다. 시감이 지나 감에 따라 라이브러리에 차용되는 예외의 처리는 점점 늘어나게 되고, 예외를 제거하는 프로그래밍은 갈수록 내구성이 약해 질것이다. 하지만, 예외처리를 배제한 컴파일을 지원하는 현재의 C++ 소프트웨어 개발상의 상태는 확실히 예외처리 보다 성능에서 우위를 점한다. 그리고 그것은 또한 예외 전달(propagate) 처리와, 예외를 생각하지 않은 라이브러리들의 사용에 무리없는 선택이 될것이다.

두번째로 try 블록으로부터의 예외를 잡는(exception-handling)에 대한 비용을 생각해 보자 이것은 당신이 catch로 예외 하나를 잡기를 원할때 마다 요구되는 비용이다. 각기 다른 컴파일러들은 서로 다른 방식으로 try블록의 적용을 한다. 그래서 해당 비용은 각 컴파일러마다 다르다. 그냥 대충 어림잡아서 예상하면, 만약 try블록을 쓰게되면, 당신의 전체적인 코드 사이즈는 5-10%가 늘어나고, 당신의 실행 시간 역시 비슷한 수준으로 늘어난다. 이제 아무런 예외를 던지지 않는다고 생각하자;우리가 여기에서 토론하고 있는것은 단지 당신의 프로그램내에서 try가 가지는 비용만이 아니다. 이런 비용의 최소화를 위해서 아마 당신은 필요하지 않는 try블럭은 피해야만 할것이다.

컴파일러는 수맣은 try 블럭의 예외 스펙에 대한 코드를 위하여 코드를 만들어내야 한다. 그래서 코드 스팩은 일반적으로 하나의 try블럭당 같은 수의 비용을 지출하게 된다. 잠깐?(excuse me?) 당신은 예외 스팩이 단시 스팩인, 즉 코드를 만들어 내는걸 생각하지 않는다고 말한다. 자, 당시은 그런 생각에 관해서 조금 새로운 몇가지를 감안해 봐ㅏ.

문제의 초점은 예외가 던지는 비용이다. 사실 예외는 희귀한 것이라 보기 때문에 그렇게 크게 감안할 내용이 아니다. 그들이 예외적인(exceptional) 문제의(event) 발생을 지칭함에도 불구하고 말이다. 80-20 규칙은(Item 16에서 언급) 우리에게 그런 이벤트들은 거의 프로그램의 부과되는 성능에 커다란 영향을 미치지 않을 것이라고 말한다. 그럼에도 불구하고, 나는 당신이 이 문제에 관하여 예외를 던지고, 받는 비용에 관한 대답에서 얼마나 클까를 궁금할것이라고 생각한다. 대강 일반적인 함수의 반환에서 예외를 던진다면 대충 세개의 명령어 정도 더 느려지는(three order of magnitude) 것이라고 가정할수 있다. 하지만 당신은 그것만이 아닐것이라고 이야기 할것이다. 반대로 당신이 이런 논쟁을 데이터 구조나 루프의 순회 구조를 효율적으로 만드는데 신경을 쓴다면 더 좋은 시간을 보내는 것이라고 생각한다.

그렇지만 잠깐, 내가 이런것에 관해서 어떻게 아냐구? 만약 예외를 위한 지원은 최근의 컴파일러와 ㄷ컴파일러간에 다른 방식으로 진행된다면서 비용이 5-10%떨어지고 스피드 역시 비슷하게 떨어지고 세개 명령어 정도 늘어나는 것과 같은 성능 저하에 관한 위의 언급 이런것에 관한 출처들? 아마 내가 해줄수 있는 답변은 다소 놀랄것이다.:당신이 try블록과 예외 스펙을 사용을 필요한 곳만 사용하도록 제한해라;그리고 컴파일 해봐라, 그래도 설계상에 문제가 있다면 일단 자신의 설계를 다시 그려보고 생각해 보라, 거기에다, 여기저기 다른 벤더들의 컴파일러로 컴파일 해봐라 그럼 알수 있다.

Retrieved from http://wiki.zeropage.org/wiki.php/MoreEffectiveC++/Exception
last modified 2021-02-07 05:23:49