U E D R , A S I H C RSS

More EffectiveC++/Exception

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블록과 예외 스펙을 사용을 필요한 곳만 사용하도록 제한해라;그리고 컴파일 해봐라, 그래도 설계상에 문제가 있다면 일단 자신의 설계를 다시 그려보고 생각해 보라, 거기에다, 여기저기 다른 벤더들의 컴파일러로 컴파일 해봐라 그럼 알수 있다.

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:23:49
Processing time 0.0899 sec