Skip to main content

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 from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("