[More Effective C++] 항목 11 ~ 12
항목 11 : 소멸자에서는 예외가 탈출하지 못하게 하자
- 클래스 소멸자가 호출되는 경우
- 객체가 통상적인 조건에서 소멸
- 지역변수 객체의 유효범위 이탈
- 객체가 직접 삭제
- 예외처리 매커니즘에 의해 객체가 소멸
- 예외 전파 시 스택 되감기를 진행한다.
- 객체가 통상적인 조건에서 소멸
- 소멸자 내부에서 예외 발생 상태인지 구별할 방법이 없다.
- 어떤 상황이든 예외 발생 상태라고 가정한다.
- 예외 처리 진행 중 다른 예외 때문에 프로그램 흐름이 소멸자 함수를 떠나면 C++는 프로그램을 강제 실행 종료한다.(지역객체조차 소멸되지 않는다.)
- 발생한 예외가 소멸자를 빠져나가지 않게 한다.
- try / catch 블록으로
- ex)
Session::~Session() { try { logDestruction(this); } catch(....) { cerr << "Unable to log destruction of Session object" << "at address" << this << ".\n"; } }
- operator« 중의 하나가 예외를 일으킬 시에 예외가 Session 소멸자를 빠져나오게 되는 문제가 있다.
- 소멸자에서 예외를 일으키는 코드를 try / catch로 그냥 묶고 아무런 처리를 하지 않는다.
- ex)
Session::~Session() { try { logDestruction(this); } catch(....) {} }
- 예외가 소멸자를 못 빠져나가게 한다는 목적에는 이것으로 충분하다.
- 소멸자에서 발생된 예외가 소멸자에서 처리되지 않으면, 소멸자는 실행이 끝나지 않은 상태로 남게 된다.
- 즉 예외가 소멸자를 빠져나가지 못하게 해야 하는 이유는
- 예외 전파 일부분으로 진행되는 스택 되감기 동작 중 terminate(프로그램의 실행을 끝장낸다)가 호출되는 것을 막는다.
- 소멸자의 동작을 완전히 끝내도록 하기 위함이다.
항목 12 : 예외 발생이 매개변수 전달 혹은 가상함수 호출과 어떻게 다른지를 이해해자.
- 매개변수와 예외는 값에 의한 전달 / 참조에 의한 전달 / 포인터에 의한 전달이 모두 가능하다.
- 함수 호출시에는 프로그램의 흐름이 함수를 호출한 부분으로 되돌아오지만, 예외를 발생시켰을 때는 흐름이 throw를 호출한 부분으로 돌아오지 않는다.
- 예외는 값에 의한 예외받기든 참조에 의한 예외받기든 상관 없이 변수의 사본이 만들어진 후 catch문으로 넘겨진다.
- ex)
istream operator>>(istream& s, Widget& w) void passAndThrowWidget() { Widget localWidget; cin >> localWidget; // 실제 객체에서 이루어지는 동작 throw localWidget; // 사본이 넘어가는 동작 }
- 프로그램의 흐름이 passAndThrowWidget을 떠나면 localWidget도 유효범위를 떠나므로 localWidget이 소멸된다. 그러므로 사본이 넘어간다.
- 만일 static Widget localWidget;으로 정적 선언을 한다 해도 복사는 이루어진다.
- 즉 예외처리는 복사해야 하므로 속도면에서 느리다.
- 예외 발생 시 객체 복사가 이루어질 때의 복사 생성자는 객체의 ‘정적’ 타입에 대응하는 클래스에 정의된 것이지 ‘동적’타입에 의해 정의된 것이 아니다.
- ex)
class Widget{....}; class SpecialWidget : public Widget {....}; void passAndThrowWidget() { SpecialWidget localSpecialWidget; Widget& rw = localSpecialWidget; throw rw; // 여기서 발생하는 예외는 Widget 타입이다. }
-
복사는 항상 정적 타입에 기반한다!!!!
- 예외는 원래 객체의 사본으로 발생하므로 catch 블록에서 예외를 전파하는 방법에도 신경을 써야 한다.
- ex)
catch(Widget& w) { throw; } // 기존 예외를 타입에 상관 없이 중계한다.(rethrow) 즉 SpecialWidget이 들어오면 SpecialWidget을 중계한다. catch(Widget& w) { throw w; } // 기존 예외의 사본이므로 Widget이 중계한다.(rethrow) SpecialWidget이 들어와도 복사가 정적 타입 기반이기 때문에 Widget을 중계한다.
- 또 다른 차이점 : 예외로서 발생되는 객체는 단순한 참조자에 의한 예외처리가 가능하다.
- 즉 예외에서는 상수참조자일 필요가 없다. 반면에 함수 호출시에는 비상수 참조자에 대한 임시객체는 매개변수로 사용될 수 없다.
- 함수에 값에 의한 인자 전달 시 복사가 일어나는 것이 예외에서도 일어난다.
- 즉 함수의 매개변수로 값이 복사 + 예외복사 매커니즘(throw)에 의해 생성되는 복사
- 똑같은 객체를 함수로 전달할 때보다 예외 전달 시 사본 생성 / 소멸이 반드시 한번 더 이루어진다.
- 포인터에 의한 예외발생은 포인터에 의한 함수 매개변수 전달처럼 포인터의 사본이 전달된다.
- 지역객체에 대한 포인터는 catch쪽으로 보내봐야 지역객체가 소멸되므로 미정의 동작이 발생할 여지가 생긴다.(이득이 없다)
- 객체가 함수 호출 / 예외발생 위치를 떠나 함수나 catch문으로 이동하는 과정도 다르다.
- ex)
double sgrt(double); int i ; double sgrtOfi = sgrt(i); void f(int value) { try { int 타입 예외 발생 } catch(double d) { .... } // 캐치하지 못한다. }
- 즉 예외는 타입 변환이 전혀 일어나지 않는다.
- catch문으로 전달되는 예외의 타입변환 상황
- 상속 기반의 변환 : 기본 클래스 예외 객체는 파생 클래스 예외 객체도 받는다. 참조 / 값/ 포인터 모두 적용된다.
- 타입이 있는 포인터에서 타입이 없는 포인터로 바뀌는 경우.
- const void*를 받는 catch문은 어떤 포인터 예외도 잡을 수 있다.
- 마지막 차이점 : catch문은 등장한 순서에 따라 사용된다. 하지만 함수는 다르다.(가상함수는 동적 타입과 가까운 클래스에 정의된 가상함수를 사용한다.)
- 가상 함수 : best fit(가장 적합한 것을 선택하는 방식)
-
예외 처리 : first fit(가장 첫째 것을 선택하는 방식)
- 차이점 정리
- 예외 객체는 항상 복사된다.
- 예외는 함수 매개변수에 비해 타입변환이 적다.
- 예외는 등장한 순서대로 동작한다.