Advanced C++

[C++] 예외 처리 Exception

로파이 2021. 2. 20. 00:25

예외 처리 Exception

 

C++ Standard Libary에서 제공하는 예외 처리를 위한 클래스로 자신의 프로그램 코드에서 어느 부분에서 예외가 발생할 것을 대비해 미리 예외 발생에 대한 디버깅 정보를 알기 위해 사용한다.

 

주로 어떤 모듈의 초기화 중 발생한 소프트웨어 에러나 메모리 할당/해제, 혹은 어떤 특정 코드 범위 내에서 발생하는 예외만 관찰하고 싶을 때 사용한다.

 

VC 런타임 헤더 vcruntime_exception.h를 보면 예외 처리 Exception 클래스에 대한 정보가 있다.

namespace std {

#pragma warning(push)
#pragma warning(disable: 4577) // 'noexcept' used with no exception handling mode specified
class exception
{
public:

    exception() noexcept
        : _Data()
    {
    }

    explicit exception(char const* const _Message) noexcept
        : _Data()
    {
        __std_exception_data _InitData = { _Message, true };
        __std_exception_copy(&_InitData, &_Data);
    }

    exception(char const* const _Message, int) noexcept
        : _Data()
    {
        _Data._What = _Message;
    }

    exception(exception const& _Other) noexcept
        : _Data()
    {
        __std_exception_copy(&_Other._Data, &_Data);
    }

    exception& operator=(exception const& _Other) noexcept
    {
        if (this == &_Other)
        {
            return *this;
        }

        __std_exception_destroy(&_Data);
        __std_exception_copy(&_Other._Data, &_Data);
        return *this;
    }

    virtual ~exception() noexcept
    {
        __std_exception_destroy(&_Data);
    }

    _NODISCARD virtual char const* what() const
    {
        return _Data._What ? _Data._What : "Unknown exception";
    }

private:

    __std_exception_data _Data;
};

특징을 보자면,

  • 복사 생성 / 대입 생성을 제공하고 기본 생성자와 const char* 문자열을 받는 생성자가 있다.
  • 문자열의 내용으로 throw할 예외에 대한 내용을 전달하여 Exception 인스턴스를 생성한다.
  • 내부에 __std_exception_data 구조체를 저장하고 있는데, 그 내용을 보면 _What이라는 문자열을 따로 저장해 두고 있다.
struct __std_exception_data
{
    char const* _What;
    bool        _DoFree;
};

_What은 실제 what()이 호출될 때 저장해두었던 문자열을 출력하기 위한 버퍼용으로 쓰이고 있다.

 

보통의 예외 처리의 로직은 Excpetion 클래스 인스턴스를 생성하자마자 throw 해준다.

class ViewApplication
{
public:
	ViewApp(){}
	bool init()
    {
    	// 디바이스 생성 Configuration
    	DeviceInfo dInfo = {}
        dInfo.osType = WINDOW;
        dInfo.memType = DEFAULT_MEM;
        ...
        // 디바이스 초기화
    	if(!InitDevice(&dInfo, &display))
        	return false;
    	return true;
    }
	Device display;
}

void Program(int num)
{
    ViewAppliation app;
    
    if(!app.init())
    {
        // throw custom standard excpetion
    	throw std::exception("App initialization failed");
    }
	
    if(num >= 10)
    {
    	// throw out_of_range which is subclass of standard exception
        throw out_of_range("out of range in array a");
    }
    int a[10] = 0;
    for(int i=0;i<num;i++)
    {
    	a[i] = i;
    }
}
int main()
{
    try()
    {
    	Program(10);
    }
    catch(const std::exception& e)
    {
    	cout << "Standard Exception" << e.what() << endl;
    }
    catch(...)
    {
    	cout << "Unknown Excpetion" << endl;
    }


	return 0;
}

위 코드의 문제는 out_of_range 인스턴스나 std::exception("App initialization failed")의 exception 인스턴스나 같은 std::exception 클래스이기 때문에 예외 처리가 구분이 되지 않는다.

 

MyException 클래스

std::exception 클래스를 상속받아 새로운 예외 처리 클래스를 정의하고 ViewApplication 인스턴스를 초기화가 실패할 때 예외 처리를 하여 "실패 상황을 구분하도록" 다음과 같이 이 MyException 인스턴스를 catch 해보는 것으로 구현한다.

int main()
{
    try()
    {
    	Program(10);
    }
    catch(const MyException& e)
    {
    	cout << "MyException Exception" << e.what() << endl;
    }
    catch(const std::exception& e)
    {
    	cout << "Standard Exception" << e.what() << endl;
    }
    catch(...)
    {
    	cout << "Unknown Excpetion" << endl;
    }

	return 0;
}

 

클래스 예시

class MyException : public exception
{
protected:
	mutable string m_whatBuffer;
private:
	int erCodeLine;
	string erFileName;
	string erMsg;
public:
	MyException(int codeLine, const char* fileName, const char* message) noexcept;
	// 예외 발생시 상세 내용 출력 인터페이스 
	virtual const char* what() const noexcept final;
	virtual const char* GetType() const noexcept;
	virtual string GetErrorMessage() const noexcept;
};

예외 처리는 실제 예외가 발생한 코드 라인과 실행 파일 이름을 받아 생성하고 what()을 호출할 시 이 내용을 출력해주도록 한다.

what()의 기존 인터페이스를 final로 설정하여 추후 MyException을 상속받는 클래스가 GetType()과 GetErrorMessage()를 오버라이드 구현을 하여 what()을 통해 재사용 가능했으면 좋겠다. GetType() GetErrorMessage() 등으로 정의해주었는데 이런 설계는 자유롭게 할 수 있다.

 

구현 예시

MyExcption::MyExcption(int codeLine, const char* fileName, const char* message) noexcept
	:
	erCodeLine(codeLine),
	erFileName(fileName),
	erMsg(message)
{
}

const char* MyExcption::what() const noexcept
{
	ostringstream oss;
	oss << "[Type] " << GetType() << endl
		<< GetErrorMessage() << endl
        	<< erMsg << endl;

	m_whatBuffer = oss.str();
	return m_whatBuffer.c_str();
}

const char* MyExcption::GetType() const noexcept
{
	return "My Exception";
}

string MyExcption::GetErrorMessage() const noexcept
{
	ostringstream oss;
	oss << "[File] " << erFileName << endl
		<< "[Line] " << erCodeLine;
	return oss.str();
}

- what buffer의 필요성

what()을 호출 할 때 내부에서 예외 발생 내용에 관한 문자열을 전달해주어야 한다. 이 전달 과정은 printf("안녕하세요");의 원리, "안녕하세요"라는 문자열을 콘솔 출력 스트림으로 전달하는 printf 함수와 마찬가지로 예외 처리 다이얼 로그에 표시되는 문자열을 찍어내는 "상상 함수" ExceptionDialog("안녕하세요");와 동일한 원리를 가진다.

const char*의 문자열, 즉 문자 배열의 포인터를 전달하는데 포인터의 실제 내용이 함수 내 지역 변수였다면 함수를 빠져나오면서 해제되므로 실제 내용은 쓰레기 값이 된다.

ex)

const char* mystring()

{

  string yeah = "yeah~";

  return yeah.c_str();

}

printf(mystring()); 

 

물론 간혹 릴리즈 모드로 빌드 시 해당 문자열 내용에 대해 c_str()를 인라인화하여 printf("yeah~");처럼 만들어 정상적으로 나오기도 한다.

대체적으로 다음과 같이 what 버퍼를 사용하지 않는다면,

const char* UserException::what() const noexcept
{
	ostringstream oss;
	oss << "[Type] " << GetType() << endl
		<< GetErrorMessage() << endl;

	// m_whatBuffer = oss.str();
	return oss.str().c_str();
}

 아래와 같은 이상한 문자열을 가진 예외 발생 다이얼 로그가 나오게 된다.

std::exception 클래스는 생성을 할 때 문자열을 whatbuffer로 저장해두었고 구현 예제에서는 새로운 문자열을 스트링 스트림으로 새로 만들어 주기 때문에 what buffer가 꼭 필요한 상황이다.

 

구현 예시처럼 정상 동작을 한다면 다음과 같이 예외 발생 다이얼로그가 생성된다.

매크로화 한 MyException 사용 예

#define MY_EXP(MSG) MyException(__LINE__, __FILE__, MSG); 로 코드 라인과 파일 이름을 가리키는 C++ Predefined Macro를 이용한다면 간단히 예외 발생을 할 수 있다.

 

C++ Predefined Macro: riptutorial.com/cplusplus/example/4867/predefined-macros

#include "MyExcpetion"
#define MY_EXP(MSG) MyException(__LINE__, __FILE__, MSG);

class ViewApplication
{
public:
	ViewApp(){}
	bool init()
    {
    	// 디바이스 생성 Configuration
    	DeviceInfo dInfo = {}
        dInfo.osType = WINDOW;
        dInfo.memType = DEFAULT_MEM;
        ...
        // 디바이스 초기화
    	if(!InitDevice(&dInfo, &display))
        	return false;
    	return true;
    }
	Device display;
}

void Program(int num)
{
    ViewAppliation app;
    
    if(!app.init())
    {
        // throw custom standard excpetion
    	throw MY_EXP("App initialization failed");
    }
	
    if(num >= 10)
    {
    	// throw out_of_range which is subclass of standard exception
        throw out_of_range("out of range in array a");
    }
    int a[10] = 0;
    for(int i=0;i<num;i++)
    {
    	a[i] = i;
    }
}
int main()
{
    try()
    {
    	Program(10);
    }
    catch(const MyException& e)
    {
    	cout << e.what() << endl;
    }
    catch(const std::exception& e)
    {
    	cout << "Standard Exception" << e.what() << endl;
    }
    catch(...)
    {
    	cout << "Unknown Excpetion" << endl;
    }


	return 0;
}