사실 예외가 발생했을 때 어떤 종류의 에러가 왜 발생했는지 상세하게 정보를 전달해야 한다.
그래야 호출원에서 에러의 종류를 판단하고 다음 동작을 결정할 수 있다. 전통적인 방법은 정수값을 리턴하여 어떤 종류의 에러가 났는지 판단하는 것이다. 하지만 에러값을 표시할 마땅한 특이값을 선정하기가 무척 어렵고 상황마다 달라야 하는 것이 번거롭다.
그렇다면 열거형은 어떤가? 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); // 직관적이지 못함
예외 클래스도 클래스이므로 상속할 수 있고 다형성도 성립한다.
#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 함수는 가상 함수로 선언되어 있어 동적 바인딩을 통해 각 객체에 맞는 에러가 출력된다고 생각하면 된다.
만약 클래스의 멤버 함수가 특정한 종류의 예외를 발생한다면 어떻게 할 것이다.
클래스 내부에 예외 클래스를 지역적으로 선언하면 이 클래스는 스스로 예외를 처리할 수 있으며 예외 처리 코드까지 포함하고 있으므로 어떤 상황에서도 예외를 처리할 수 있다.
그래서 애초에 클래스를 설계할 때부터 예외 처리를 포함하고 있는 것이 좋다.
#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() 처럼 하나하나 다 처리해줘야 하기 때문이다.
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++ 표준 예외 (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 |