Computer Science/C & C++

[C++] Exceptions | 예외

lww7438 2020. 7. 13. 16:56

Exceptions

예외

- 예측하지 못한, 프로그램을 정상적으로 실행할 수 없는 상황에 대한 대처방법을 기술하는 기능이다.
- 구형 컴파일러에서는 지원되지 않을 수 있다.
- 일부 컴파일러에서는 Exceptions 기능을 Disable해 놓는 것이 Default Options이다.


\(\texttt{std::abort()}\) 함수

- \(\texttt{cstdlib}\) 또는 \(\texttt{stdlib.h}\) 헤더파일에 정의되어 있는 함수이다.
- \(\texttt{cerr}\)가 사용하는 스트림(표준 에러 스트림)에 "abnormal program termination" 메시지를
  출력하고, 프로그램을 종료시킨다.
  (\(\texttt{abort()}\)를 호출한 함수를 종료하는 것이 아닌, 프로그램 자체를 종료시킨다.)
- \(\texttt{abort()}\) 함수를 호출한 프로그램이 다른 프로그램에 의해 구동되는 Child Process(자식 프로세스)인 경우,
  Parent Process(부모 프로세스) 혹은 OS(운영체제)에게 컴파일러에 종속적인 특정값을 리턴하게 된다.
- \(\texttt{abort()}\) 함수가 File Buffer*를 비우는지에 대한 여부는 시스템마다 상이하다.
- \(\texttt{exit()}\) 함수를 명시적으로 사용하여 메시지를 출력하지 않고, File Buffer만 비울 수 있다.

* File Buffer
- 파일에 전송할 내용, 파일로부터 전송받을 내용을 저장하는 데 사용되는 메모리 영역을 의미한다.

// Harmonic Mean (조화 평균) 계산 프로그램
// abort() 함수를 이용하여 예외상황을 비정상 종료시키는 코드

#include <iostream>
#include <cstdlib>

double hmean(double a, double b);

int main() {
	double x, y, z;
	std::cout << "Enter two numbers: ";
	while (std::cin >> x >> y) {
		z = hmean(x,y);
		std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl;
		std::cout << "Enter next set of numbers <q to quit>: ";
	}
	std::cout << "Bye!\n";
	return 0;
}

double hmean(double a, double b) {
	if (a == -b) {
		std::cout << "untenable arguments to hmean()\n";
		std::abort();
	}

	return 2.0 * a * b / (a + b);
}

Returning an Error Code (에러 코드 리턴)

- 함수의 리턴값을 이용하여 문제의 종류를 구분지을 수 있게하는 방법이다.
- 예외를 무조건 비정상 종료시키는 방법보다, 예외 상황에 더 유연하게 대처하는 형태이다.

ex) ostream 클래스의 get(void) 메서드는 파일 내용의 끝(예외 상황)을 감지하면 EOF를 리턴한다.

// Harmonic Mean (조화 평균) 계산 프로그램
// 에러 코드를 리턴함으로써 예외상황에 대처하는 코드

#include <iostream>
#include <cfloat> // (or float.h) for DBL_MAX

bool hmean(double a, double b, double * ans);

int main() {
	double x, y, z;
	std::cout << "Enter two numbers: ";
	while (std::cin >> x >> y) {
		if (hmean(x,y,&z))
			std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl;
		else
			std::cout << "One value should not be the negative " << "of the other - try again.\n";
		
                std::cout << "Enter next set of numbers <q to quit>: ";
	}
	std::cout << "Bye!\n";
	return 0;
}

bool hmean(double a, double b, double * ans) {
	if (a == -b) {
		*ans = DBL_MAX;
		return false;
	}

	else {
		*ans = 2.0 * a * b / (a + b);
		return true;
	}
}


※ 리턴 조건을 위 프로그램처럼 포인터/참조형 매개변수로 넘기는 대신,
   전역변수로 선언하여 호출하는 함수가 항시 확인할 수 있도록 하는 기법도 있다.
   (전통적인 C math Library에서 사용되는 방법이다.)


C++ Exception Mechanism (C++ 예외 메커니즘)

- C++에서 제공하는 예외 처리 메커니즘은 예외가 발생한 부분에서
  미리 지정해 놓은 지점(Exception Handler)으로 제어를 넘기는 방식으로 진행된다.
- 예외가 발생하면, 발생한 부분의 함수를 종료하고, try Block이 기술되어 있는 함수를 발견할 때 까지
  함수들을 거슬러 올라간다.
   

※ C++ 예외 처리 메커니즘의 절차
1. Throwing an exception (예외 발생)
2. Catching an exception with a handler (핸들러를 이용한 예외 포착)
3. Using a \(\texttt{try}\) block (\(\texttt{try}\) 블록 사용)

Keyword: \(\texttt{
throw}\)
- 예외 발생 시, 출력할 내용을 기술한다.
- \(\texttt{throw}\) 키워드의 뒤에는 예외 상황을 기술한 문자열 혹은 객체와 같은 하나의 값을 기술한다.
- \(\texttt{throw}\)문은 기본적으로 객체의 복사본을 \(\texttt{catch}\) Block에 전달한다.
  (\(\texttt{catch}\) Block과 \(\texttt{throw}\)가 넘기는 객체의 Scope가 겹치지 않는 경우가 더 많기 때문이다.) 

throw "Unexpected error.";

※ 또한, \(\texttt{throw}\) 뒤에 아무런 객체도 위치시키지 않음으로써,
   그 위치(Callee)에서 Caller로 제어를 넘기는데 이용하기도 한다.
- 기본적으로, \(\texttt{throw}\)문이 실행되면 Unwinding Stack(스택 풀기)가 수행되면서,
  Callee에서 Caller의 \(\texttt{catch}\) Block으로 제어가 옮겨진다.
   (\(\texttt{catch}\) Block에서 사용하여 예외를 재발생시켜, 해당 함수를 한 층 더 벗어나게 한다.)

throw;


Keyword: \(\texttt{catch}\)
- Exception Handler(포착한 예외에 대한 처리방법)을 기술한다.
  (Exception Handler = \(\texttt{catch}\) Block)
- \(\texttt{throw}\) 키워드로부터 넘겨받은 객체의 데이터 타입을 소괄호 안에 명시해야 한다.
  (\(\texttt{throw}\)가 넘긴 객체의 데이터 타입과 \(\texttt{catch}\)에서 명시한 데이터 타입이 일치해야 해당 Exception Handler가 동작한다.)

- \(\texttt{catch}\) 키워드는 예외 발생 시, 프로그램 제어를 넘겨받기 위한 레이블의 역할을 한다.
  (즉, \(\texttt{catch}\) Block은 함수가 아니다.)

catch (const char* s) {
    cout << s << std::endl;
    ...
}

※ \(\texttt{catch}\) Block에서는 \(\texttt{throw}\)로 부터 객체의 복사본을 넘겨받는다.
- \(\texttt{catch}\) Block의 데이터형을 참조로 선언한다 해도, 복사본을 넘겨받게 된다.
- 객체가 해당 Scope를 넘어가서 전달되기 전에 소멸되는 것을 방지하기 위함이다.
- 그럼에도 불구하고, 참조 선언을 가능하게 한 이유는 상속 관계에 있는 객체를 모두 아우를 수 있게하기 위함이다.*
* 참조형으로 선언된 Base Class Object는 Derived Class Object도 참조할 수 있다. (역은 성립하지 않는다.)

※ 각각의 상속 레벨에 대한 \(\texttt{catch}\) Block이 수행할 작업을 모두 다르게 지정하고자 하는 경우,
   아래 코드와 같이, \(\texttt{catch}\) Block을 상속 계층의 역순으로 정의해야 한다.
   (기초 클래스 객체는 모든 파생 클래스 객체를 아우를 수 있지만, 파생 클래스 객체는 기초 클래스 객체를 받을 수 없기 때문이다.)

class bad_1 {...};
class bad_2 : public bad_1 {...};
class bad_3 : public bad_2 {...};

...

void duper() throw (bad_1) {
    if(specific_cond_1)
        throw bad_1();
    if(specific_cond_2)
        throw bad_2();
    if(specific_cond_3)
        throw bad_3();
}

...

try {
    duper();
}
catch(bad_3& B) { ... }   // 말단 파생 클래스에 대한 catch Block
catch(bad_2& B) { ... }   // 중간 파생 클래스에 대한 catch Block
catch(bad_1& B) { ... }   // 기초 클래스에 대한 catch Block

// 상속 레벨에 따라 catch의 동작 내용을 달리하고자 한다면,
// 위와 같이, 상속 관계의 최하단에 있는 파생 클래스에 대한 catch문부터 작성해야 한다.


- 넘겨받을 객체의 데이터 타입 대신, ELIPSIS(...)를 명시하여, 모든 예외를 처리하게 할 수 있다.

try { 
    throw specific_object_1();
}

catch(bad_3& be) { ... }
catch(bad_2& be) { ... }
catch(bad_1& be) { ... }
catch(...) { }       // 괄호 안에 "..."은 ELIPSIS를 의미하는 C++예약어이다. 모든 예외를 가리지 않고 받아들인다.
                     // switch문의 default문과 유사한 기능을한다.
                     // 즉, 예외가 bad_3, bad_2, bad_1에도 해당되지 않으면 이 catch문이 동작하게 된다.



Keyword: \(\texttt{try}\)
- \(\texttt{try}\) 키워드로 정의된 \(\texttt{try}\) Block은 예외들이 발생할 수 있는 가능성이 있는 코드들이 위치하는 코드 블럭이다.
- \(\texttt{try}\) 블럭 뒤에는 하나 이상의 Exception Handler(\(\texttt{catch}\) Block)이 뒤따르게 된다.
- \(\texttt{try}\) 블럭에서 예외가 발생하지 않았다면, 뒤따르는 \(\texttt{catch}\) 블럭들은 모두 무시된다.

try {
    c = division(a, b);  // a / b에서 b가 0이면 예외가 발생한 것으로 처리하는 것이 바람직할 것이다.
    ...
}



// Harmonic Mean (조화 평균) 계산 프로그램
// C++ 예외 처리 메커니즘(try - throw - catch)을 이용하여 예외상황에 대처하는 코드

#include <iostream>

double hmean(double a, double b);

int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        try {
        // start of try block
            z = hmean(x,y);
        } // end of try block

        catch (const char * s) {
        // start of exception handler
            std::cout << s << std::endl;
            std::cout << "Enter a new pair of numbers: ";
            continue;    // catch 블록은 함수가 아니므로, 이와 같이 while문에 대한 continue문을 사용할 수 있다.
        } // end of handler

        std::cout << "Harmonic mean of " << x << " and " << y << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }

    std::cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b) {
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";

    return 2.0 * a * b / (a + b);
}


※ \(\texttt{throw}\)문에 일치하는 데이터 타입을 가진 \(\texttt{catch}\) Block이 없거나, \(\texttt{try}\) Block이 없으면, \(\texttt{abort()}\) 함수가 호출된다.

Program flow with exceptions


※ \(\texttt{return}\)문과 \(\texttt{throw}\)문의 차이

return vs throw


Using Objects as Exceptions (예외로써 객체 사용하기)

- 예외로써 서로 다른 데이터 타입을 사용하여 예외가 발생된 상황을 구별할 수 있다.
- 또한, 객체 자체에 예외 사항에 대한 정보를 담아 넘길 수도 있다.

// Harmonic Mean (조화 평균), geometric Mean (기하 평균) 계산 프로그램
// C++ 예외 처리 메커니즘(try - throw - catch)을 이용하여 예외상황에 대처하는 코드
// 예외로써 객체를 넘긴다.

// exc_mean.h -- exception classes for hmean(), gmean()
#include <iostream>

class bad_hmean {
private:
    double v1;
    double v2;
public:
    bad_hmean(double a = 0, double b = 0) : v1(a), v2(b){}
    void mesg();
};

inline void bad_hmean::mesg() {
    std::cout << "hmean(" << v1 << ", " << v2 <<"): " << "invalid arguments: a = -b\n";
}

class bad_gmean {
public:
    double v1;
    double v2;
    bad_gmean(double a = 0, double b = 0) : v1(a), v2(b){}
    const char * mesg();
};

inline const char * bad_gmean::mesg() {
    return "gmean() arguments should be >= 0\n";
}
// ---------------------------------------------------------------------



//error4.cpp – using exception classes
#include <iostream>
#include <cmath> // or math.h, unix users may need -lm flag
#include "exc_mean.h"

double hmean(double a, double b);
double gmean(double a, double b);

int main() {
    using std::cout;
    using std::cin;
    using std::endl;
    double x, y, z;
    cout << "Enter two numbers: ";
    while (cin >> x >> y) {
        try { // start of try block
            z = hmean(x,y);
            cout << "Harmonic mean of " << x << " and " << y << " is " << z << endl;
            cout << "Geometric mean of " << x << " and " << y << " is " << gmean(x,y) << endl;
            cout << "Enter next set of numbers <q to quit>: ";
        }// end of try block

        catch (bad_hmean & bg) { // start of catch block
            bg.mesg();
            cout << "Try again.\n";
            continue;
        }
        
        catch (bad_gmean & hg) {
            cout << hg.mesg();
            cout << "Values used: " << hg.v1 << ", " << hg.v2 << endl;
            cout << "Sorry, you don't get to play any more.\n";
            break;
        } // end of catch block
    }

    cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b) {
    if (a == -b)
        throw bad_hmean(a,b);
    
    return 2.0 * a * b / (a + b);
}

double gmean(double a, double b) {
    if (a < 0 || b < 0)
        throw bad_gmean(a,b);

    return std::sqrt(a * b);
}
// ---------------------------------------------------------------------

Exception Specifications Meet C++11 (C++11에서의 예외 사양)

- C++98에서 추가되었으며 C++11에 접어서면서 그 중요성이 떨어지고 있는 개념이다.
※ 이 개념은 추후, 표준에서 제외될 가능성이 있으므로, 가급적 사용을 자제해야 한다.

void func_1(int n) throw(bad_thing);
// 함수 func_1에서는 bad_thing 타입의 예외가 발생할 가능성이 있다.

void func_2(int n) throw();
// 함수 func_2에서는 예외를 발생시키지 않으나, 예외를 발생시키는 함수를 호출할 가능성이 있다.

void func_3(int n) noexcept;
// 함수 func_3는 예외를 발생시키지 않는다.

- 위와 같은 함수 이름 끝의 throw() 문은 함수 원형 혹은 함수 정의부에서 기술될 수 있다.
- 사용자게에 try Block을 사용할 필요가 있음을 알리기 위한 명시적인 표현방법이다.
- 또한, 컴파일러가 런타임 체크용 코드를 추가하여 예외 사항이 위반되었는지에 대한 여부를 확인할 수 있게 한다.

* \(\texttt{noexcept(operand)}\) Operator
- operand가 예외를 발생시킬 수 있는지에 대한 여부를 알리는 연산자이다.
- operand가 예외를 발생하지 않는다는 보장이 있으면 \(\texttt{true}\)를 리턴하고, 그렇지 않으면 \(\texttt{false}\)를 리턴한다.

void func_A(int n);
void func_B(int n) noexcept;

...

noexcept(func_A); // false를 리턴한다.
noexcept(func_B); // true를 리턴한다.

\(\texttt{exception}\) Class (\(\texttt{exception}\) 클래스)

- \(\texttt{exception}\) 클래스에 정의된 표준 예외는 아래와 같다.
- \(\texttt{bad_alloc}\)
- \(\texttt{bad_cast}\)
- \(\texttt{bad_exception}\)
- \(\texttt{bad_typeid}\)
- \(\texttt{bad_function_call}\)
- \(\texttt{bad_weak_ptr}\)
- \(\texttt{logic_error}\)
- \(\texttt{runtime_error}\)

- \(\texttt{exception}\) 헤더파일 (\(\texttt{exception.h}\) 또는 \(\texttt{except.h}\))에 정의되어 있다.
- \(\texttt{exception}\)클래스는 \(\texttt{std}\) namespace내에 분류되어 있다.
- \(\texttt{exception}\) 헤더파일에서는 \(\texttt{unexpected()}\) 함수에 의해 \(\texttt{bad_exception}\)을 사용할 수 있다.

※ 클래스 정의 시, \(\texttt{exception}\) 클래스를 상속받아서 \(\texttt{exception}\) 객체에 대한 \(\texttt{catch}\) Block을 이용해 모든 파생 객체들에 대한 예외를 한꺼번에 처리할 수 있다.

#include <exception>

class bad_hmean : public std::exception {
public:
    const char * what() { return "bad arguments to hmean()"; }
    ...
};

class bad_gmean : public std::exception {
public:
    const char * what() { return "bad arguments to gmean()"; }
    ...
};



try { ... }

catch(std::exception & e) {
    cout << e.what() << endl;
    ...
}


* \(\texttt{what()}\) 메서드

- 발생한 Exception 객체의 종류를 문자열로 리턴하는, \(\texttt{exception}\) 클래스에 정의된 메서드이다.

※ 예외는 하나의 클래스이다.
- 하나의 예외 클래스를 다른 클래스로부터 파생시킬 수 있다.
- 클래스 정의 안에 예외 클래스 선언을 내포시켜 예외를 클래스에 병합시킬 수 있다.
- 내포된 예외들 또한 상속될 수 있으며, 기초 클래스의 역할을 할 수 있다.

* 예외 클래스를 내포시킨 클래스 예시

#include <stdexcept>
#include <string>
using std::string;

class Sales
{
public:
	enum {MONTHS = 12}; // could be a static const

	class bad_index : public std::logic_error
    { // 배열 인덱스 범위를 벗어난 값을 저장하는 클래스  
	  // 클래스 외부에서는 Sales::bad_index
      // 클라이언트 프로그램의 catch 블록에서 bad_index를 하나의 예외 객체로 사용할 수 있다.
	private:
		int bi; // bad index value
	public:
		explicit bad_index(int ix,
		const std::string & s = "Index error in Sales object\n");
		int bi_val() const {return bi;}
		virtual ~bad_index() throw() {}
	};

	explicit Sales(int yy = 0);
	Sales(int yy, const double * gr, int n);
	virtual ~Sales() { }
	int Year() const { return year; }
	virtual double operator[](int i) const;
	virtual double & operator[](int i);

private:
	double gross[MONTHS];
	int year;
};

Sales::bad_index::bad_index(int ix, const string & s )
	: std::logic_error(s), bi(ix) { }

Sales::Sales(int yy)
{
	year = yy;
	for (int i = 0; i < MONTHS; ++i)
		gross[i] = 0;
}

Sales::Sales(int yy, const double * gr, int n)
{
	year = yy;
	int lim = (n < MONTHS)? n : MONTHS;
	int i;
	for (i = 0; i < lim; ++i)
		gross[i] = gr[i];
	
    // for i > n and i < MONTHS
	for ( ; i < MONTHS; ++i)
		gross[i] = 0;
}

double Sales::operator[](int i) const
{
	if(i < 0 || i >= MONTHS)
		throw bad_index(i);
	return gross[i];
}

double & Sales::operator[](int i)
{
	if(i < 0 || i >= MONTHS)
		throw bad_index(i);
	return gross[i];
}



class LabeledSales : public Sales
{
public:
	
    class nbad_index : public Sales::bad_index
	{ // bad_index로부터 파생되며, LabeledSales 객체의 레이블을 저장 및 보고하는 기능이 추가되었다.
      // 클래스 외부에서는 LabeledSales::nbad_index
	private:
		std::string lbl;
	public:
		nbad_index(const std::string & lb, int ix, const std::string & s = "Index error in LabeledSales object\n");
		const std::string & label_val() const {return lbl;}
		virtual ~nbad_index() throw() {}
	};
	
    explicit LabeledSales(const std::string & lb = "none", int yy = 0);
	LabeledSales(const std::string & lb, int yy, const double * gr, int n);
	virtual ~LabeledSales() { }
	const std::string & Label() const {return label;}
	virtual double operator[](int i) const;
	virtual double & operator[](int i);

private:
	std::string label;
};

LabeledSales::nbad_index::nbad_index(const string & lb, int ix, const string & s )
	: Sales::bad_index(ix, s)
{
	lbl = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy)
	: Sales(yy)
{
	label = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy, const double * gr, int n)
	: Sales(yy, gr, n)
{
	label = lb;
}

double LabeledSales::operator[](int i) const
{
	if(i < 0 || i >= MONTHS)
		throw nbad_index(Label(), i);
	return Sales::operator[](i);
}

double & LabeledSales::operator[](int i)
{
	if(i < 0 || i >= MONTHS)
		throw nbad_index(Label(), i);
	return Sales::operator[](i);
}



// use_sales.cpp -- nested exceptions
#include <iostream>
#include "sales.h"
int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	double vals1[12] =
	{
		1220, 1100, 1122, 2212, 1232, 2334,
		2884, 2393, 3302, 2922, 3002, 3544
	};
	double vals2[12] =
	{
		12, 11, 22, 21, 32, 34,
		28, 29, 33, 29, 32, 35
	};
	Sales sales1(2011, vals1, 12);
	LabeledSales sales2("Blogstar",2012, vals2, 12 );
	cout << "First try block:\n";
	try
	{
		int i;
		cout << "Year = " << sales1.Year() << endl;
		for (i = 0; i < 12; ++i)
		{
			cout << sales1[i] << ' ';
			if (i % 6 == 5)
				cout << endl;
		}
		cout << "Year = " << sales2.Year() << endl;
		cout << "Label = " << sales2.Label() << endl;
		for (i = 0; i <= 12; ++i)
		{
			cout << sales2[i] << ' ';
			if (i % 6 == 5)
				cout << endl;
		}
		cout << "End of try block 1.\n";
	}
    
	catch(LabeledSales::nbad_index & bad)
	{
		cout << bad.what();
		cout << "Company: " << bad.label_val() << endl;
		cout << "bad index: " << bad.bi_val() << endl;
	}
    
	catch(Sales::bad_index & bad)
	{
		cout << bad.what();
		cout << "bad index: " << bad.bi_val() << endl;
	}
    
	cout << "\nNext try block:\n";
    
	try
	{
		sales2[2] = 37.5;
		sales1[20] = 23345;
		cout << "End of try block 2.\n";
	}
    
	catch(LabeledSales::nbad_index & bad)
	{
		cout << bad.what();
		cout << "Company: " << bad.label_val() << endl;
		cout << "bad index: " << bad.bi_val() << endl;
	}

	catch(Sales::bad_index & bad)
	{
		cout << bad.what();
		cout << "bad index: " << bad.bi_val() << endl;
	}
    
	cout << "done\n";
	return 0;
}

※ C++98까지는 Destructor에 throw() 예외를 사용할 수 있었다.
※ C++11부터는 Destructor에 throw() 예외를 사용할 수 없게 되었다. 


\(\texttt{stdexcept}\) Header File (\(\texttt{stdexcept}\) 헤더파일)

- \(\texttt{stdexcept}\) 헤더파일에는 \(\texttt{logic_error}\) Exception Family, \(\texttt{runtime_error}\) Exception Family가 정의되어 있다.
- 이 두 Exception Family는 \(\texttt{exception}\)을 \(\texttt{public}\)으로 상속받은 클래스이다.

\(\texttt{logic_error}\) Exception Class Family
- 프로그램 수정을 통해 극복이 가능한것으로 판단되는 예외들로 구성된다.
- 아래 4개의 예외는 \(\texttt{logic_error}\)에 속하는 예외들이다.
- \(\texttt{domain_error}\) Class : 기대한 범위를 넘어선 값이 함수에 전달되었을 때 예외를 발생시킨다.
- \(\texttt{invalid_argument}\) Class : 기대하지 않은 형태의 값이 함수에 전달되었을 때 예외를 발생시킨다.
- \(\texttt{length_error}\) Class : 원하는 액션을 취할 만한 충분한 공간이 확보되지 못했을 때 예외를 발생시킨다.
ex) \(\texttt{string}\) 클래스의 \(\texttt{append()}\)메서드는 특정 문자열이 최대 허용 길이를 초과했을 때 \(\texttt{length_error}\)를 발생시킨다.
- \(\texttt{out_of_bounds}\) Class : 인덱싱 에러를 감지하면 예외를 발생시킨다.

\(\texttt{runtime_error}\) Exception Class Family
- 불가피한 예외들로 구성된다.
- 아래 3개의 예외는 \(\texttt{runtime_error}\)에 속하는 예외들이다.
- \(\texttt{range_error}\) Class : 오버/언더플로우가 발생하지 않은 상태에서 치역범위를 넘어섰을 때, 예외를 발생시킨다.
- \(\texttt{overflow_error}\) Class : 오버플로우가 발생되었을 때, 예외를 발생시킨다.
- \(\texttt{underflow_error}\) Class : 언더플로우가 발생되었을 때, 예외를 발생시킨다.

※ C++ 상속 메커니즘에 따라, 계층적으로 Exception Class Family를 아우르는 \(\texttt{catch}\) Block 또한 충분히 작성 가능하다.

try {
...
}

catch(out_of_bounds & oe) // catch out_of_bounds error
{...}

catch(logic_error & oe) // catch remaining logic_error family
{...}

catch(exception & oe) // catch runtime_error, exception objects
{...}




bad_alloc Exception

- C++ 시스템에서 메모리 할당에 문제가 발생한 경우에 발생시키는 예외이다.
- bad_alloc 클래스는 exception 클래스를 public으로 상속받은 클래스이다.
- bad_alloc 클래스는 new 헤더파일 (또는 new.h)에 정의되어 있다.

* C++에서는 메모리 할당 시, 문제가 발생했을 때 두 가지 방법으로 대처한다.
1. new가 null을 리턴한다.
2. bad_alloc 예외를 발생시킨다.
- 컴파일러 스위치 등과 같은 방법을 통해 둘 중 하나를 Default로 세팅할 수 있다.

* 예외 발생 시, new가 null을 리턴하도록 하는 Flag 설정법
- new에 nothrow를 설정한다.

int* pi = new(std::nothrow) int;

if(pi == 0){
    cout << "Rejected Allocation" << std::endl;
    ...
}



* bad_alloc 핸들링 예시

// newexcp.cpp -- the bad_alloc exception
#include <iostream>
#include <new>
#include <cstdlib> // for exit(), EXIT_FAILURE
using namespace std;
struct Big
{
	double stuff[20000];
};
int main()
{
	Big* pb;
	try {
		cout << "Trying to get a big block of memory:\n";
		pb = new Big[1000000000];
		cout << "Got past the new request:\n";
	}
	catch (bad_alloc& ba)
	{
		cout << "Caught the exception!\n";
		cout << ba.what() << endl;
		exit(EXIT_FAILURE);
	}
	cout << "Memory successfully allocated\n";
	pb[0].stuff[0] = 4;
	cout << pb[0].stuff[0] << endl;
	delete[] pb;
	return 0;
}

Unexpected Exception (기대하지 않은 예외)
- throw()나 noexcept와 같은 Exception Specification(예외 사양)에 지정되어 있지 않은 곳에서 예외가 발생한 경우를 의미한다.
- 감지되면, 프로그램 실행을 중지시킨다.
- 함수에 Exception Specification(예외 사양, 예외 지정)을 사용함으로써, 해당 함수를 사용하는 사용자들은 그 함수에서 어떤 예외들이 발생할 수 있는지를 사전에 알 수 있다.

double Argh(double, double) throw(out_of_bound);
// 여기서 throw(out_of_bound) 구문이 예외 사양에 해당된다.
// Argh() 함수에서는 out_of_bound 타입의 예외가 발생될 수 있음을 의미한다.

try {
    x = Argh(a, b);
}
catch (out_of_bound& ex) {    // Argh() 함수에서 발생할 수 있는 out_of_bound 예외에 대비한다.
    ...
}


※ 원칙적으로, 예외 지정에는 해당 함수에서 발생될 수 있는 모든 예외들이 명시되어야 한다.
- 가령, A() 함수에서는 a라는 예외가 발생할 수 있고, B() 함수에서는 b라는 예외가 발생할 수 있으며, A() 함수에서 B() 함수를 호출한다고 하면, A() 함수에서의 예외 지정에는 a와 b에 대한 예외 지정이 모두 이루어져야 한다는 뜻이다.
- 이는 예외 규격 메커니즘이 좁은 범뮈내에서 사용되어야 함을 의미한다.
- 이러한 점으로 인해, C++11에서 예외 규격(예외 지정) 개념을 제외하고자 하는 것이다.

* Unexpected Exception가 발생하면, 프로그램은 unexcepted() 함수를 호출한다.
- unexcepted() 함수는 기본적으로 terminate() 함수를 호출하고,
  terminate() 함수는 기본적으로 abort() 함수를 호출한다.
- set_unexcepted() 함수를 통해, unexcepted() 함수의 동작을 변경할 수 있다.
- 특히, set_unexcepted() 함수는 set_terminate() 함수보다 더욱 세밀하게 제어할 수 있다.
- 위 함수들 모두 exception 헤더파일에 선언되어 있다.

typedef void (*unexpected_handler)();

unexpected_handler set_unexpected(unexpected_handler f) throw(); // C++98
unexpected_handler set_unexpected(unexpected_handler f) noexcept; // C++11

void unexpected(); // C++98
void unexpected() noexcept; // C++0x


* unexcepted_handler 함수는 아래와 같은 기능을 할 수 있다.
1. terminate() 함수(디폴트), abort() 함수, exit() 함수로 대체되어 프로그램을 종료시킬 수 있다.
2. 예외를 발생시킬 수 있다.
- 발생한 예외가 예외 지정에 일치하면, 그에 대응되는 catch Block을 찾는다.
- 발생한 예외가 예외 지정에 일치하지는 않으나, 예외 지정에 std::bad_exception형이 명시되어 있으면,
  해당 예외는 bad_exception형 예외로 간주된다.
- 발생한 예외가 예외 지정에 일치하지 않고, 예외 지정에 std::bad_exception형조차 명시되어 있지 않으면,
  프로그램은 terminate() 함수를 호출한다.

* unexcepted_handler 함수가 예외를 발생시키고, 이를 적절히 처리하는 구조

#include <exception>   // 예외에 관련된 여러가지가 정의되어 있다.

void myUnexcepted() {
    throw std::bad_exception();    // throw;문만 적어도 무방하다.
}

...

set_unexcepted(myUnexpected);

...

double Argh(double, double) throw(out_of_bounds, bad_exception);

...

try {
    x = Argh(a, b);
}
catch (out_of_bound& ex) {
    // out_of_bound형 예외에 대한 대처사항
}
catch (bad_exception& ex) {
    // bad_exception형 예외에 대한 대처사항
}

Uncaught Exception (포착되지 않은 예외)
- try Block 밖에서 발생되거나, 어떠한 catch Handler와도 매치되지 않는 예외가 발생된 경우를 의미한다.
- 감지되면 terminate() 함수를 호출하며, 프로그램을 즉각 중단시키지 않는다.
- terminate() 함수는 기본적으로 abort() 함수를 호출하며, abort() 함수 대신, 다른 함수를 호출하도록 변경할 수 있다.
- terminate() 함수가 호출하는 함수는 set_terminate() 함수를 이용하여 바꿀 수 있다.
- terminate() 함수와 set_terminate() 함수는 exception 헤더파일에 선언되어 있다.

typedef void (*terminate_handler)();
// terminate_handler : 매개변수를 사용하지 않고, 리턴값도 없는 함수를 가리키는 포인터를 위한 데이터형

terminate_handler set_terminate(terminate_handler f) throw(); // C++98
terminate_handler set_terminate(terminate_handler f) noexcept; // C++11
// set_terminate() 함수는
// 매개변수를 사용하지 않고 리턴형이 void인 함수(terminate_handler 타입)의 이름(=주소)을 매개변수로 사용한다.
// 이전에 등록된 함수의 주소를 리턴한다.
// set_terminate() 함수가 한 번 이상 호출되면,
// terminate() 함수는 가장 최근의 set_terminate() 함수 호출에 의해 설정된 함수를 호출한다.

void terminate(); // C++98
void terminate() noexcept; // C++11
// 예외가 발생했으나, 포착되지 않으면, 프로그램은 terminate() 함수를 호출한다.
// terminate() 함수는 기본적으로 abort() 함수를 호출하게 되어있다.
// set_terminate() 함수를 이용하여 terminate() 함수가 abort() 함수 대신, 다른 함수를 호출하게 할 수 있다.


* 시스템은 Uncaught Exception가 발생하면, terminate() 함수를 호출하고,
  사용자의 의도에 따라 terminate() 함수는 myQuit() 함수를 호출한다.

void myQuit() {
    std::cout << "포착되지 않은 예외가 발생하여 프로그램을 중지시킵니다.\n";
    exit(5);
}

...

set_terminate(myQuit);   // 프로그램의 시작 부분에 위치해야 한다.

Exception Cautions (예외 주의사항)

* 예외의 단점
- 예외를 사용함으로써 프로그램의 크기가 커지고, 실행속도를 저해한다.
- 예외 지정은 템플릿 개념과 어울리지 않는다.
  (템플릿을 통한 여러 종류의 특수화로 인해 여러 종류의 예외가 발생될 수 있기 때문이다.
   즉, 템플릿 기능은 발생될 예외의 예측을 어렵게 하기 때문이다.)
- 예외는 동적 메모리 할당 개념과 어울리지 않는다.
  (메모리 반납이 이루어지기 전에, 예외가 발생되어 throw문이 실행되고 프로그램이 종료되면,
   반납되지 못한 메모리로 인한 Memory Leak이 발생한다.

void test2(int n) {
 	double* ar = new double[n];
 	...
 	if (oh_no)
 	    throw exception();  // 메모리 반납 이전에 예외가 발생되면, throw문으로 인해 프로그램이 종료된다.
 	...
 	delete[] ar;    // 메모리 반납
 	return;
 }


* 프로그래머가 Memory Leak에 대비하여, catch Block에 따로 메모리 반납 코드를 삽입함으로써,
  예외 처리로 인한 Memory Leak을 방지할 수 있다.

void test3(int n) {
	double * ar = new double[n];
	...
	try {
		if (oh_no)
		throw exception();
	}
	catch(exception & ex)
	{
		delete [] ar;  // 예외가 발생한 경우의 메모리 해제 코드
		throw;
	}
	...
	delete [] ar;    // 예외가 발생되지 않은 경우의 메모리 해제 코드
	return;
}

- 이 방법은 프로그래머의 실수로 해제를 빠뜨리거나, 다른 에러를 발생시킬 가능성이 있어 완벽한 해결책이라 할 수 없다.
- 이에 대한 해결책으로, auto_ptr 템플릿이 있다.