C++ of the Day #42 - Use of std::bad_exception

이번 글은 Use of std::bad_exception을 보고 작성하였습니다.

아래 프로그램의 실행 결과는 무엇일까요?

void my_unexpected() {
if (!uncaught_exception())
cerr << "my_unexpected() called\n";
throw;
}

void my_terminate() {
cerr << "my_terminate() called\n";
}

void func() {
cerr << "func() called\n";
}

void g() throw (int) {
throw 1.0; // throws double
}

int main()
{
set_unexpected (my_unexpected);
set_terminate (my_terminate);
atexit (func);
try {
g();
}
catch (int) {
cerr << "caught int\n";
}
catch (bad_exception& e) {
cerr << "caught bad_exception\n";
}
return 0;
}


한 단계씩 살펴보도록 하겠습니다. 먼저 main() 함수의 처음 세 라인은 각각 unexpected, terminate 함수와 프로그램 종료시 호출될 함수를 등록하고 있습니다. 그리고 C++03에서 unexpected() 함수와 관련된 내용은 다음과 같습니다.


  1. exception-specification을 가진 함수에서 exception-specification에 리스트되어 있지 않은 예외를 던지면 호출된다.


  2. unexpected 함수안에서 exception-specification에 있는 예외를 던지면 해당 handler를 찾아 처리한다.


  3. unexpected 함수안에서 rethrow 하거나 exception-specification에 없는 예외를 던지면...

    • exception-specification에 std::bad_exception이 있으면 던져진 예외는 std::bad_exception으로 치환되어 처리된다.

    • exception-specification에 std::bad_exception이 없으면 terminate() 함수가 호출된다.






이제 main() 함수를 살펴 보죠. try 블록 안에서 g() 함수를 호출하고 있는데 g() 함수의 exception-specification에는 int 타입만이 존재합니다. 하지만 g() 함수에서는 double 타입을 던지고 있죠. 따라서 제일 먼저 unexpected 함수가 호출됩니다.

다음으로 unexpected() 함수에서는 원래 예외를 그대로 rethrow하고 있는데 g() 함수의 exception-specification에는 std::bad_exception이 없습니다. 따라서 terminate() 함수가 호출됩니다. terminate() 함수는 abort() 함수를 호출하게 되고 abort() 함수의 경우 atexit()로 등록한 함수들이 호출되지 않게 됩니다. ((C++03, 3.6.3.4. Calling the function void abort(); declared in <cstdlib> terminates the program without executing destructors for objects of automatic or static storage duration and without calling the functions passed to atexit(). ))

따라서 위 프로그램의 결과는 다음과 같습니다.


my_unexpected() called
my_terminate() called
Abort (core dumped)


그럼 위 프로그램에서 g() 함수의 exception-specification에 다음과 같이 std::bad_exception을 추가하면 결과는 어떻게 될까요?


void g() throw (std::bad_exception, int) {


unexpected에서 rethrow한 double 타입의 예외는 std::bad_exception으로 치환되어 처리됩니다. 그리고 exception handler에 std::bad_exception을 처리하는 handler가 있으므로 이 handler에서 처리되고 프로그램은 정상적으로 종료됩니다.

프로그램 종료시에 atexit()로 등록한 함수가 호출되므로 프로그램의 결과는 다음과 같습니다.


my_unexpected() called
caught bad_exception
func() called


마지막으로 위의 프로그램에서 아직 설명하지 않은 함수가 하나 있는데 바로 uncaught_exception()입니다. C++03에는 다음과 같이 설명되어 있습니다.


18.6.4 uncaught_exception [lib.uncaught]
bool uncaught_exception() throw();
1 Returns: true after completing evaluation of a throw-expression until either completing initialization of the exception-declaration in the matching handler or entering unexpected() due to the throw; or after entering terminate() for any reason other than an explicit call to terminate(). [Note: This includes stack unwinding (15.2). --end note]

2 Notes: When uncaught_exception() is true, throwing an exception can result in a call of terminate() (15.5.1).


간단히 설명하면 예외가 던져진 후부터 어떤 handler가 catch 하기 전까지만 true를 리턴하는 함수입니다. 당연하게도 handler가 다시 rethrow하게 되면 다른 handler가 받기 전까지 다시 true를 리턴합니다. ((기본적인 uncaught_exception() 함수의 동작을 위해 여기 코드를 참고하세요.))

위의 코드와 같이 던져진 예외에 의해 호출된 unexpected() 함수안에서 uncaught_exception() 함수의 리턴 값은 위에 언급된 대로 true가 되어야 합니다. 하지만 위의 결과를 보면 my_unexpected() 함수안에서 호출된 uncaught_exception() 함수가 false를 리턴했음을 알 수 있습니다. 이 점에 있어서는 g++가 표준을 따르지 않음을 알 수 있습니다. ((여기를 보니 VC++에서는 무조건 false를 리턴하도록 구현되어 있군요.))

그럼 왜 표준에선 이런 함수를 정의해 놓았을까요? 이유는 exception이 처리되면서 발생하는 stack unwinding과 관계가 있습니다. stack unwinding이 시작되면 해당 stack에 있는 객체들의 소멸자들이 호출됩니다. 그런데 exception에 의한 stack unwinding시 호출되는 소멸자에서 다시 exception이 던져되면 바로 terminate()가 호출됩니다. ((C++03, 15.2.3. [Note: If a destructor called during stack unwinding exits with an exception, terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. --end note] ))

이런 이유로 소멸자에선 예외가 빠져나가게 해서는 안된다라는 규칙도 생긴거죠. 이런 경우 uncaught_exception을 사용하여 소멸자에서 uncaught_exception의 리턴 값이 false인 경우에만 예외를 던지게 되면 terminate가 불리지 않게 할 수 있습니다. ((물론 Uncaught Exceptions에 보면 이런 사용법은 여러가지 문제가 있기 때문에 사용해서는 안됩니다.)) 이런 관점에서 봤을 때는 현재 g++에서의 구현이 C++03보다 더 맞는 것으로 보이기도 합니다. unexpected() 함수에서는 예외를 던지더라도 terminate()가 호출되진 않으니까요. :-)

마지막으로 A Pragmatic Look at Exception Specifications에 있듯이 exception specification의 사용은 권장되지 않습니다.

사용이 권장되지 않는 exception specification과 uncaught_exception() 함수에 대해 너무 길게 써버렸네요. :-)

Comments

  1. C++의 세계는 끝이 없이 깊은 것 같습니다. 그래도 C++ 무척 좋아요. 흐흐

    ReplyDelete
  2. 네, 좀 그렇죠? 어느 정도 깊이까지는 알아야겠지만 그 이후부턴 취미(?)삼아 해야 할 것 같습니다. :-) 표준에도 애매한 부분이 많아서 머리가 아프죠.

    그나저나 이번 글은 너무 비실용적이군요. :-?

    ReplyDelete
  3. VC++ 에서 무조건 false 를 리턴하지는 않는것 같습니다. http://msdn.microsoft.com/en-us/library/k1atwat8(VS.80).aspx 코드로 테스트해 보니, try 블록 내부에서 발생된 예외에 대해서는 true를 리턴하고 외부에서 발생된 것에대해서는 false 를 리턴됩니다.

    제가 틀릴수도 있으니, 한번 살펴 보시면 좋을것 같습니다. 그나저나 이 함수 잘 이해가 안되네요. 쓰라는건지 말라는건지. :)

    ReplyDelete
  4. 제가 걸었던 링크는 VC++.NET 2003 버전이었네요. 버전 올라가면서 수정되었나봐요.
    이글 쓴게 1년도 넘었으니 이해해주세요~ :)

    ReplyDelete
  5. 해당 함수는 more exceptional C++ 에선 사용하지 않은게 좋지 않겠는가? 라고 권유하고 있네요. MSVC만해도 자사 컴파일러 특성을 타는 코드이니 그럴만도 할거 같습니다.

    싸이트에 재미있는 글이 많아 자주 들러봅니다^^

    ReplyDelete
  6. 자주 들리신다니 감사... 1년도 넘어 방치 상태인데 다시 손을 대볼까 생각중입니다. :)

    ReplyDelete

Post a Comment

Popular Posts