Post List

2015년 2월 9일 월요일

C++ Exception Handling (예외 처리) 와 Stack Unwinding (스택 풀기)

* Exception Handling (예외 처리)

프로그램 실행 도중에 비정상적인 상황이 발생하는 것을 예외(Exception)이라고 하고,
그 상황을 처리하는 과정을 예외 처리(Exception Handling)이라고 한다.
C++에서 예외를 처리하는 구문은 아래와 같다.

try
{
  if (/* Exception Condition */)
    throw new std::exception("Error Description");
}
catch (std::exception e)
{
std::cout << "Exception : " << e.what() << std::endl;
}

예외처리를 하지 않을 경우 실행할 일반적인 code 들은 try {...절 안에 둔다.
예외가 발생하는 상황에서는 throw를 이용하여 예외를 발생시키면 catch (...) {...}절 에서 해당 예외를 처리해주는 code를 넣으면 된다.
catch (...) {...}절은 예외 종류에 따라 여러개를 둘 수 있다.
다른 예외들을 하나의 catch (...) {...}절에서 처리 할 필요없이 다른 절에서 받아서 각각 처리가 가능하다.

예외가 발생하는 대표적인 예는 특정 수를 0 으로 나눌 경우이다.

#include <iostream>

void main()
{
    int a = 9;
    int b = 0;
    int c = a / b;
    std::cout << c << std::endl;
}

위의 짧은 Code 실행시 Debug 모드에서는 아래와 같은 오류를 발생시키며, Release 모드로 실행시킬땐 비정상 종료시키는 창까지 뜬다.



굳이 예외를 쓰지않고 프로그램 로직으로도 처리하는 방법도 있다.

void main()
{
    int a = 9;
    int b = 0;
    int c = 0;

    if (b != 0)
    {
       c = a / b;
       std::cout << c << std::endl;
    }
    else
       std::cout << "Exception : Divide by zero" << std::endl;
}

하지만 이렇게 하면 예외가 발생할때마다 처리해 주어야 할 code는 늘어난다.
어차피 예외로 처리해도 늘어나긴 하지만, code 상에서 예외 처리 구문과 원래 프로그램 code의 구분이 명확하지가 않아서 나중에 보고 파악하는게 어려워진다.

그럼 try {...catch (...) {...}를 이용하여 예외 처리하는 형식으로 고쳐보자.

#include <iostream>

void main()
{
    int a = 9;
    int b = 0;
    int c = 0;

    try
    {
       if (b == 0) throw b;
       c = a / b;
       std::cout << c << std::endl;
    }
    catch (int divided)
    {
       std::cout << "Exception : Divide by " << divided << std::endl;
    }
}

원래 code보다는 좀 더 분명하게 예외 처리하는 부분을 명확히 구분 할 수 있게 되었다.
되도록이면 try {...} 절에 들어가는 code를 최소화로 하는게 나중에 code를 다시 봤을 때 이해하기가 편하다.

하지만 대부분의 경우 저렇게 try {...} 안에서 바로 throw로 예외를 전달하는 경우는 잘 없다.
보통 함수에서 예외를 발생시키고 해당 함수를 호출하는 측에서 try {...catch (...) {...}절을 이용하여 예외를 처리한다.

통상적으로 많이 사용하는 형태는 아래와 같다.

#include <iostream>

template <typename T>
T Divide(T a, T b)
{
    if (b == 0) throw std::exception("Divide by zero");
    return a / b;
}

void main()
{
    int a = 9;
    int b = 0;
    int c = 0;

    try
    {
       c = Divide<int>(a, b);
       std::cout << c << std::endl;
    }
    catch (std::exception e)
    {
       std::cout << e.what() << std::endl;
    }
}

* 함수 선언에 예외 정보 포함시키기

함수를 선언할 때 함수 원형 뒤쪽에 이 함수에서 발생 할 수 있는 예외의 종류를 지정할 수 있다.

throw( ... ) 에서 괄호 안에 예외의 타입을 넣어주면 된다.
이걸 안적어준 경우는 임의외 예외를 던질 수 있다는 의미이다.

void func(int a) throw(int);

예외가 2개 이상일 경우 콤마(,)로 구분하여 나열한다.

void func(int a) throw(char *, int);

괄호안에 아무것도 안적어주면 예외를 던지지 않는 함수라는 의미이다.

void func(int a) throw();

* 표준 예외 클래스

자주 발생하는 예외에 대해서 미리 정의한 예외 클래스 들이 있다.
각각의 예외별로 다른 header에 정의되어 있지만,
#include <exception>만 해주더라도 대부분의 경우 사용이 가능하다.

#include <iostream>
#include <new>

void main()
{
    char* ptr;
    try
    {
       ptr = new char[(~unsigned int((int)0) / 2) - 1];
       delete[] ptr;
    }
    catch (std::bad_alloc &ba)
    {
       std::cout << ba.what() << std::endl;
    }
}

표준 예외 클래스 몇개만 나열해 보겠다.

bad_alloc: 메모리 할당 오류로서 new 연산에서 발생 <new>
bad_cast : 형변환 오류로서 dynamic_cast 에서 발생  <typeinfo.h>
bad_type_id : typeid에 대한 피 연산자가 널 포인터인 경우 발생
bad_exception : 예기치 못한 예외로서 함수 발생 목록에 있지 않는 예외
bad_logic_error : 클래스 논리 오류로 invalid_argumentlength_errorout_of_range 의 기본 <stdexcept>
runtime_error : 실행 오류로 overflow_errorunderflow_error의 기본 <stdexcept>

* Stack Unwinding (스택 풀기)

위에서 본 예제들 같이 예외를 catch해줘서 처리를 해 줘야하는데, 만약 catch절이 없는 상태에서 예외가 발생하면 어떻게 될까 ?
그러면 해당 함수를 호출한 곳으로 그 예외를 넘기게 된다.
만약 그 함수에서도 예외를 catch하여 처리하지 못하면 또 그 위에 함수로 계속해서 예외는 전달된다.
이렇게 예외를 처리하지 못하고 상위 함수로 계속 전달하는 과정을 스택 풀기(Stack Unwinding) 이라고 한다.
왜 이런 이름이 붙여 졌는지를 간단히 설명하자면...
함수를 호출하게 되면 이전 함수의 정보를 Stack에 넣어둔다. 그래서 함수를 계속해서 호출하게되면 그 정보들이 Stack에 계속 누적되고, 함수의 처리가 끝나면 Stack에서 정보를 찾아서 자신을 호출한 곳으로 돌아간다.
예외 처리를 위해서 계속해서 Stack 을 올라가면서 호출한 함수들의 실행을 풀어버린다는 뜻이다.

#include <iostream>

void f1() { throw 0; }
void f2() { f1(); }
void f3() { f2(); }
void f4() { f3(); }

void main()
{
    try
    {
       f4();
    }
    catch (int e)
    {
       std::cout << e << std::endl;
    }

}

위 code 실행시 호출된 순서대로 Stack에 쌓이게 된다.



main() -> f4() -> f3() -> f2() -> f1() 의 순서대로 호출된다.
그런데 f4()에서 오류가 발생하면 해당 오류를 처리해 줄수 있는 함수까지 역순으로 찾아간다.
먼저 f1()를 Stack에서 꺼내서 해제한 후
f2()를 꺼낸 뒤 오류 처리를 해 줄수 있는지 보고 없으면 f2()도 해제 한 후
f3() -> f4() -> main() 순으로 Stack에서 해제하게 된다.

위 code를 실행하면 f1에서 발생한 예외를 처리하기 위하여 f2 -> f3 -> f4 를 거쳐서 main() 에서 처리를 하게 된다.

* 참조한 곳

MSDN의 여러 곳 ( 다 나열하기엔 너무 많아서... )
EXYNOA NETWORK : http://blog.eairship.kr/179
알고보면 재미있는 IT&RIVEW BLOG : http://algobomyun.tistory.com/265

댓글 없음:

댓글 쓰기