상세 컨텐츠

본문 제목

C++ 예외 객체

C++/C++98

by deulee 2023. 8. 8. 21:02

본문

1. 예외를 전달하는 방법

사실 예외가 발생했을 때 어떤 종류의 에러가 왜 발생했는지 상세하게 정보를 전달해야 한다.

 

그래야 호출원에서 에러의 종류를 판단하고 다음 동작을 결정할 수 있다. 전통적인 방법은 정수값을 리턴하여 어떤 종류의 에러가 났는지 판단하는 것이다. 하지만 에러값을 표시할 마땅한 특이값을 선정하기가 무척 어렵고 상황마다 달라야 하는 것이 번거롭다.

 

그렇다면 열거형은 어떤가? C++의 에러 처리 구문을 이용하여 다음의 예시를 간단하게 만들어 볼 수 있을 것이다.

#include <iostream>

enum E_Error { OUTOFMEM, OVERRANGE, HARDFULL };
void Calc()throw(E_Error)
{
	if (/* 예외 발생*/) throw OVERRANGE;
}

int main(void)
{
	try {
		Calc();
	}
	catch(E_Error e) {
		switch (e) {
			case OUTOFMEM:
				...
			case OVERRANGE:
				...
			case HARDFULL:
				...
		}
	}
	return 0;
}

열거형의 에러 값은 정수형보다 의미가 좀 더 분명하지만 호출원에서 에러의 의미를 일일이 기억하고 해석해야 한다느 점에 있어서는 여전히 불편하다.

 

예외를 일으키는 쪽에서 예외의 의미까지 전달하도록 하는 것이 예외 객체의 개념이다. 다음은 예외 객체의 예시를 보도록 하자.

#include <iostream>

class Exception
{
private:
	int ErrorCode;

public:
	Exception(int ae) : ErrorCode(ae)
	{}
	int GetErrorCode()
	{
		return ErrorCode;
	}
	void ReportError()
	{
		switch (ErrorCode) {
			case 1:
				std::cout << "메모리 부족" << std::endl;
				break;
			case 2:
				std::cout << "범위 초과" << std::endl;
				break;
		}
	}
};

void Calc()
{
	if (/* 에러 발생 */) throw Exception(1);
}

int main(void)
{
	try {
		Calc();
	}
	catch(Exception& e) {
		std::cerr << e.GetErrorCode << std::endl;
		e.ReportError();
	}
	return 0;
}

"Calc" 함수는 에러가 발생했을 때 에러에 대응되는 예외 객체를 생성하여 이 객체를 "throw"로 던지고 "catch"는 예외 객체의 레퍼런스를 받아 예외 객체로부터 에러 코드를 얻고 에러 메시지 출력을 예외 객체에게 시킨다.

 

"throw"는 던지는 예외 객체의 복사본을 생성하고 이 복사본을 던진다. 이때 레퍼런스로 복사본을 받는 것이 좋다. 왜냐하면 call by value로 하기에는 객체의 재 생성이 부담되고 포인터로 하기에는 포인터 값을 보내기 위해 다음 처럼 적어야 하기 때문이다.

 

throw &Exception(1); // 직관적이지 못함


2. 예외 클래스 계층

예외 클래스도 클래스이므로 상속할 수 있고 다형성도 성립한다.

#include <iostream>

class Exception
{
protected:
	int Number;
public:
	Exception(int n) : Number(n)
	{}
	virtual void printError()=0;
};

class ExNegative : public Exception
{
public:
	ExNegative(int n) : Exception(n)
	{}
	virtual	void printError()
	{
		std::cout << "음수이므로 잘못된 값이다." << std::endl;
	}
};

class ExTooBig : public Exception
{
public:
	ExTooBig(int n) : Exception(n)
	{}
	virtual	void printError()
	{
		std::cout << "너무 크다." << std::endl;
	}
};

class ExOdd : public Exception
{
public:
	ExOdd(int n) : Exception(n)
	{}
	virtual	void printError()
	{
		std::cout << "홀수이므로 잘못된 값이다." << std::endl;
	}
};

int main(void)
{
	while (1)
	{
		int n;
		try {
			std::cin >> n;
			if (n == 0) break;
			if (n < 0) throw ExNegative(n);
			if (n > 100) throw ExTooBig(n);
			if (n % 2 != 0) throw ExOdd(n);
		}
		catch (Exception& e) {
			e.printError();
		}
	}
	return 0;
}

이렇게 catch 구문에서 Exception& e의 객체의 정적 타입은 Exception 클래스이지만 동적 타입은 throw로 던져지는 객체에 따라 달라지게 된다.

 

그리고 각 클래스의 printError 함수는 가상 함수로 선언되어 있어 동적 바인딩을 통해 각 객체에 맞는 에러가 출력된다고 생각하면 된다.


3. 예외와 클래스

만약 클래스의 멤버 함수가 특정한 종류의 예외를 발생한다면 어떻게 할 것이다.

 

클래스 내부에 예외 클래스를 지역적으로 선언하면 이 클래스는 스스로 예외를 처리할 수 있으며 예외 처리 코드까지 포함하고 있으므로 어떤 상황에서도 예외를 처리할 수 있다.

 

그래서 애초에 클래스를 설계할 때부터 예외 처리를 포함하고 있는 것이 좋다.

#include <iostream>

class MyClass
{
public:
	class Exception
	{
	private:
		int ErrorCode;
	public:
		Exception(int ae) : ErrorCode(ae)
		{}
		int GetErrorCode()
		{
			return ErrorCode;
		}
		void ReportError()
		{
			switch (ErrorCode) {
				case 1:
					break;
				case 2:
					break;
			}
		}
	};
	void Calc() {
		try {
			if (/* 에러 발생 */) throw Exception(1);
		}
		catch (Exception& e) {
			std::cout << GetErrorCode() << std::endl;
			e.ReportError();
		}
	}
	void Calc2() throw(Exception){
		if (/* 에러 발생 */) throw Exception(2);
	}
};

int main(void)
{
	MyClass M;
	M.Calc();
	try {
		M.Calc2();
	}
	catch (MyClass::Exception& e) {
		std::cout << GetErrorCode() << std::endl;
		e.ReportError();
	}
	return 0;
}

우선 클래스의 멤버 함수 Calc의 내부 Exception 지역 클래스를 사용하여 예외를 처리하고 있다.

 

그리고 "MyClass::Exception& e"를 통해 외부에서도 참조하는 모습을 볼 수 있다. 이게 가능하려면 내부 Exception 지역 클래스를 public으로 선언해야만 한다.

 

반드시 이렇게 해야 하는 이유는 private으로 선언하게 될 경우 멤버 함수 내에서 Calc() 처럼 하나하나 다 처리해줘야 하기 때문이다.


4. 생성자와 연산자의 예외

C++의 예외 처리 기능은 생성자와 연산자에서도 사용할 수 있다.

 

특히 생성자의 경우 리턴값이 없기 때문에 통상적인 방법으로는 예외를 처리하기가 무척이나 까다로웠다. 그리고 연산자의 경우 연산 결과와 에러 값이랑 구분이 안되기 때문이다.

#include <iostream>

class Int100
{
private:
	int num;
public:
	Int100(int an)
	{
		if (an > 100)
			throw an;
		else
			num = an;
	}
	Int100& operator+=(int b)
	{
		if (num + b > 100)
			throw num + b;
		else
			num += b;
		return *this;
	}
};

int main(void)
{
	try {
		Int100 i(1000);
	}
	catch (int e) {
		std::cout << e << " is bigger than 100" << std::endl;
	}
	return 0;
}

위의 예제에서 생성자와 연산자는 적절한 예외 처리를 하고 있다. 생성자로 전달된 인수가 100보다 클 경우 생성을 중지하고 연산자 오버로딩에서도 합이 100이 넘으면 예외를 던지는 것을 볼 수 있다.

 

하지만 위의 예시는 그닥 좋은 예시라고 할 수가 없다.

 

왜냐하면 "Int100"의 객체 "i"는 "try" 구문에서의 지역으로 선언되어있기 때문이다. 즉, try 구문 밖으로 나가면 객체의 지역이 끝나 파괴된다. try 외에서는 사용할 수 없는 것이다.

 

그래서 보통은 생성자는 예외 처리 구문을 쓰는 대신 성공적인 생성 여부를 표시하는 별도의 멤버를 두고 객체 생성 후에 이 멤버의 값을 평가하는 방법을 더 많이 사용한다.

 

생성자는 객체 생성에 실패할 경우 "성공 여부 플래그에 에러 코드를 대입"해 놓고 객체를 쓰는 쪽에서 이 플래그를 점검한다.

'C++ > C++98' 카테고리의 다른 글

C++ 표준 예외  (0) 2023.08.08
C++ try 블록 함수  (0) 2023.08.08
C++ 예외 처리  (0) 2023.08.08
C++ 클래스 템플릿  (0) 2023.08.08
C++ 함수 템플릿  (0) 2023.08.08

관련글 더보기