try~catch 와  __try~__except 의 차이에 대해 물어보시는 분들이 종종 계셔서 한번 정리해보았습니다.


I. 예외(Exception)을 핸들링하는 세 가지 방법


windows 에서 C/C++로 소프트웨어를 개발할 때 예외(Exception)을 핸들링할 수 있는 방법에는 크게 세가지가 있습니다.


1. __try ~ __except

Windows OS에서 제공하는 예외 처리 방법이며, 일반적으로 SEH (Structured Error Handling)이라고 부릅니다.

Access Violation 과 같이 프로그램 상의 오류(버그)로 인해 발생하는 대부분의 예외를 처리 가능합니다.


예를 들어, 다음과 같은 코드가 있다면


실행 결과는 다음과 같이 됩니다.

__try

__except!!

Finish Function!

계속하려면 아무 키나 누르십시오 . . .


__try ~ __except 구문의 가장 큰 단점은 소멸자가 존재하는 클래스와 함께 사용할 수 없다는 점인데요.. 이 부분은 이 포스트의 마지막 부분에서 다시 살펴보겠습니다.



2. try ~ catch

표준 C++에서 제공하는 예외 처리 방법이며, 일반적으로 C++ Error Handing이라고 부릅니다.

try ~ catch 구문은 throw 키워드를 이용해 코드상에서 명시적으로 발생시킨 예외를 핸들링합니다.

try ~ catch 구문은 내부적으로는 __try ~ __except 의 예외처리 구조 (SEH) 를 기반으로 구현되어 있습니다.


예를 들어, 다음과 같은 코드를...


C++ Exception Handling을 이용해 다시 작성하면 다음과 같이 됩니다.


아무래도 정리 코드가 중복되지 않고 하다보니.. 좀 더 깔끔해보이죠.

catch에서 일치되는 DataType이 없어 예외가 핸들링되지 않는다면... 프로그램은 최종적으로 예외를 발생시키고 비정상 종료되게 됩니다.


3. Unhandled Exception Filter

이것은... 말 그대로 SEH나 C++ 예외핸들링에 의해 Handling되지 않은 예외를 최종적으로 핸들링할 수 있는 Callback 함수를 등록하는 방법입니다.

다음 포스트를 참고하세요.

http://kuaaan.tistory.com/103


보통 __except 구문이나.. Unhandled Exception Handler에는 프로세스 메모리덤프를 작성하거나, 예외 내용이나 콜스택 등의 정보를 로깅하는 코드가 들어가게 됩니다.


UnhandledExceptionHandler 의 가장 큰 단점은.. 예외를 통지받을 수는 있지만 핸들링한 후에 (예외를 발생시킨 코드를  skip하고) 계속해서 실행시키기는 어렵다는 점입니다. 보통은... 메모리덤프나 로그를 작성한 다음 프로세스를 조용히 종료시키거나 재시작시키는 등의 용도로 사용합니다.



IItry~catch 을 __try~__except 처럼 사용하기

그렇다면... C++의 예외 핸들링 (try~catch)으로 Accecc Violation 같은 종류의 예외는 핸들링할 수 없는 걸까요??


다음과 같이 Virual Studio의 컴파일 옵션에서 설정할 수 있습니다.

C/C++ > Code Generation > Enable C++ Exceptions



각 항목의 설정에 따라 바이너리의 동작은 다음과 같이 달라집니다.


Compile Option try~catch 에서 일반 예외 (SEH) 처리
Yes with SEH Exceptions (/EHa) 가능
Yes (/Ehsc, /EHs) 불가 (==> Visual Studio 2010부터 Default 설정)
No 가능


즉, /EHa 로 설정하면 try ~ catch 블럭에서도 SEH와 같은 에러 핸들링이 가능합니다.

샘플을 보면 다음과 같습니다.


위의 코드를 "/EHa" 옵션으로 빌드하여 실행시키면 다음과 같이 됩니다.

try

before exception

catch!!

계속하려면 아무 키나 누르십시오 . . .


이걸 "/EHsc" 옵션으로 빌드하여 실행시키면... 이렇게 되죠.



^^;;;;


III. __try ~ __except 구문과 소멸자를 가진 클래스를 함께 사용하기

반면에 __try 구문의 가장 큰 단점은... __try를 사용하는 함수에서는 소멸자가 정의된 클래스를 사용할 수 없다는 점입니다.

예를 들어 다음과 같은 코드는 빌드 오류를 발생시킵니다.


1>------ Build started: Project: TestProject, Configuration: Release Win32 ------

1>Build started 2014-08-11 오후 2:44:19.

1>TestProject.cpp(98): warning C4509: nonstandard extension used: 'TryExcept' uses SEH and 'Dummy' has destructor

1>          TestProject.cpp(85) : see declaration of 'Dummy'

1>TestProject.cpp(98): error C2712: Cannot use __try in functions that require object unwinding

1>

1>Time Elapsed 00:00:00.57

========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========


그래서 저처럼 소멸자를 이용해 장난치는 걸 좋아하는 개발자들은... __try를 그닥 좋아하지 않습니다. ^^;;;


그런데 SEH 관련 빌드 옵션을 정리한 다음 테이블을 보시면... /EH 옵션을 주지 않을 경우 위의 코드가 빌드될 수 있다는 것을 알 수 있습니다. 대신... 예외가 발생할 경우 소멸자 호출이 되지 않는다는군요... 


Compile Option__try ~ __except 와 소멸자를 가진 객체를 함께 사용
Yes with SEH Exceptions (/EHa)불가 (빌드 오류)
Yes (/Ehsc, /EHs)불가 (빌드 오류)
No

가능 (대신, 소멸자 호출이 보장되지 않음 )
Compile warning 발생


예외 발생시 핸들링을 하는 대신 소멸자 호출을 포기할지는 개발자 개개인의 선호도에 따라 선택할 문제겠지만... 

만약 저 소멸자 내에 메모리를 해제하는 코드가 들어있었다면?? 그냥 메모리 릭이 좀 발생하고 말았을 것입니다. 그런데 소멸자 내에 CriticalSection을 Unlock하는 코드가 들어있었다면....?? 그렇다면 예외 발생이 결과적으로 Dead Lock으로 귀결될 수도 있는 일입니다. 이건 받아들이기 어렵죠. ^^;;


대신 다음과 같은 식으로는 가능합니다. (모든 스레드에 대해 아래와 같은 방식으로 사용)


위와 같이 코드를 작성하여 "/EHa" 옵션으로 빌드하면... 빌드도 가능하고 다음과 같이 동작도 정확하게 이루어집니다. 

( 뭐 어차피 /EHa 옵션을 주고 빌드할 거라면 try ~ catch(...)을 사용하면 되니 굳이 __try~__except를 사용할 필요는 없겠지만요... 굳이 사용한다면... 그렇다는 겁니다.. ㅎㅎ )


Dummy!

Use Dummy Class!!

~Dummy!

__except!!

Finish Function!

계속하려면 아무 키나 누르십시오 . . .


(참고로... 위의 코드를 /EHsc 옵션으로 빌드하면 다음과 같이 소멸자가 정상적으로 호출되지 않습니다.)

before exception

Dummy!

__except!!

Finish Function!

계속하려면 아무 키나 누르십시오 . . .



즉.. 특별한 이유가 없다면 그냥 /EHa 를 쓰면 된다는 거죠.

그렇다면 VisualStudio 2010은 왜 디폴트 설정이 /EHa 가 아닌 /EHsc일까요? 제 생각엔 이것은 아마 성능과 관련된 이유 때문일 것 같습니다.


요약하자면.... 만약 다음과 같은 기능들을 원한다면.... 

  1. 복수의 워커스레드로 구성된 스레드풀을 사용하면서,
  2. 소멸자가 있는 클래스(예 : 스마트포인터, 자동 CriticalSection 등)를 자유롭게 사용하고, 
  3. 워커스레드에서 예외 발생시 메모리 덤프를 작성하는 등의 예외 핸들링을 수행.
  4. 예외 핸들링 후 프로세스를 종료하지 않고 새로운 Job을 계속 실행
아래과 같이 코드를 작성한 후 "/EHa" 옵션을 주어 빌드하면 되겠군요. ^^

 
뭐 대충 아래와 같이 try~catch를 사용해도 큰 차이는 없을 것 같습니다. 



IV. 일반적인 예외(SEH)를 C++ 예외로 변환하기 (_set_se_translator)

음... 위의 코드가 다 좋긴 한데요... catch(...) 내에서는 예외 정보 (예외코드나 컨텍스트 정보 같은거...)를 얻을수 없다는 아쉬움이 있죠. 왜냐하면... GetExceptionCode() 나 GetExceptionInformation() 와 같은 api들을 catch블럭 안에서 사용할 수 없기 때문이죠. ^^;;


그래서... SEH 예외가 발생했을 때 이것을 C++ 예외로 변환하는 방법이 있습니다. 

Handles Win32 exceptions (C structured exceptions) as C++ typed exceptions.

일단 저기다가... 콜백을 등록해 놓으면 __try~__except 에 예외가 탐지되었을 때 콜백이 옵니다. 그럼 그 콜백 안에서 적당히 C++ 예외를 생성해서 다시 throw해주면 되는 거죠... ^^


아래는 샘플코드입니다. (msdn에 있는 샘플코드를 제가 약간 수정했습니다... 그 샘플코드가 약간... 1@#$!@#%해서요. ^^)


In trans_func. : Exception is detected!!! (ExceptionCode c0000094)

Caught a __try exception with SE_Exception. ErrorCode : c0000094, Eip : 001E1E63

In finally


In trans_func. : Exception is detected!!! (ExceptionCode c0000094)

Caught a __try exception with SE_Exception. ErrorCode : c0000094, Eip : 001E1E63

In finally


In trans_func. : Exception is detected!!! (ExceptionCode c0000094)

Caught a __try exception with SE_Exception. ErrorCode : c0000094, Eip : 001E1E63

In finally





Posted by kuaaan
,


사랑합니다. 편안히 잠드소서