C/C++의 예외 모델 차이

try { throw } catch {} & do { break } while (false) 에서 트랙백. C++의 예외 처리가 느리기 때문에 do while을 이용한 코드 블럭 탈출 / setjmp/longjmp를 이용한 함수간 예외 전달을 다뤘길래 문제가 있다고 생각하는 점을 지적하려고 포스팅.

C에서는 예외의 발생을 일반적으로 에러 번호를 통해서 전파하며, 특정 블럭의 실행을 끝내기 위해서

 do {
 // do some thing
} while ( 0 );

같은 코드 블럭에서 break 문을 사용하는 방법을 많이 쓴다. (goto문과 비슷하지만 label 문제도 없고 상대적으로 보기가 쉬우면서도 C++에서 쓰기도 편하다) 혹은 setjmp / longjmp라는 그렇게 널리 권장된지 않는 방법을 쓰기도 한다. 이건 진정한 의미1의 goto문이기 때문에 C 에서도 매우 주의 깊게 사용해야하고 – 꼭 필요한지 3번은 되묻자 – C++에서는 사실상 사용이 금지 된다.

C++의 경우에는 예외적인 경우가 발생했을 때 말 그대로 예외exception을 생성시키고 이를 던진다throw. 문제는 C++의 경우 C 보다 강력한 표현력을 제공하고 OOP를 지원하기 위한 장치들이 들어가 있다는 것인데 가장 중요한 내용은 바로 stack-unwinding이라고 부르는 현상을 일으킨다는 것이다.(이것 때문에 많이 느려질 수 밖에 없다)

바로 예외가 블럭 스코프를 벗어날 때 해당 블럭에서 생성했던 – auto, register로 선언한 변수 – 들의 소멸자를 일일이 호출해주는 것인데, 이를 위해서 C++ 컴파일러는 예외가 생길 수 있는 부분마다 이런 변수들의 위치와 소멸자를 추적해줘야한다. 그렇기 때문에 예외가 발생하게 되면 이런 정보를 토대로 일일이 변수를 찾아서 소멸자를 호출해주게 되는데, 이 과정은 “예외는 정말 가끔 일어나는 경우다” 라는 가정하에 그다지 최적화되지도 않고해서 굉장히 느리다.

트랙백한 글에서 이를 비교해서 문제 삼고 있다.(사실 문제인게 맞다) 그렇지만 do { code block } while( 0 ); 와는 달리 예외는 좀 더 단순하고 생각하기 쉬운 제어 경로를 보여주고, setjmp/longjmp의 문제를 안 일으킨다. setjmp/longjmp를 사용하는 경우 말 그대로 실행 경로만 바뀌기 때문에 stack 위에 쌓여있을 변수들이 소멸되지 않아서(…), 동적으로 할당했거나 혹은 추가한 참조등에 대해서 처리가 되지 않는다. 이런 코드가 있다고 치자.

{
     // scope를 벗어나면 소멸자에서 lock을 해제한다.
     scope_based_lock lock(lock_name);

     if(exception_condition) {
         longjmp(jmp_buf_var);
     }
}

이런 코드인 경우에 longjmp는 재앙이다. 특정 리소스에 대한 lock이 해제되지 않는다. 그렇기 때문에 – 이 경우 말고도 메모리 할당 / 스마트 포인터의 참조 포인터 처리 등 소멸자에서 처리하는 부분이 있으면 – 여러가지 문제로 인해서 C++에서는 longjmp의 사용이 사실상 금지된다.

반면에 저 상황에서 longjmp가 아니라 예외를 던졌다면, stack-unwinding에 의해 소멸자가 적절히 호출되고 리소스 lock이 해지되지 않는 일은 발생하지 않는다. 예외를 주의깊게 정말 예외로 처리해야할 상황 에서 썼다면 예외 처리로 인한 속도 문제는 큰 무언가가 아니다. 암달의 법칙을 기억하라. 실제로 예외가 발생해서 해당 부분의 코드가 사용되는 시간은 실제 가동 시간의 아주아주아주아주 작은 일부분이다. 그러니 안심하고 C++ 친화적인 예외exception을 사용하자.


  1. setjmp로 특정 위치를 기록하고, longjmp를 호출하면 setjmp를 호출했던 장소로 즉시 이동한다. 같은 함수 혹은 스코프 안에서 setjmp/longjmp가 불릴 필요가 없기 때문에 함수 간에도 이동이 가능하다(…). ↩︎