본문 바로가기

개발 공부/C++

따라하며 배우는 C++ 14. 예외 처리

따라하며 배우는 C++ 14. 예외 처리

 

14.1 예외처리Exception Handling의 기본

 

#include <iostream>
#include <fstream>
#include <string>

using namespace std;

int findFirstChar(const char* string, char ch)
{
	for (size_t index = 0; index < strlen(string); ++index)
	{
		if (string[index] == ch)
			return index;
	}
	return -1;
}

double divide(int x, int y, bool& success)
{
	if (y == 0)
	{
		success = false;
		return 0.0;
	}
	success = false;
	return static_cast<double>(x) / y;
}


int main()
{
	bool success;
	double result = divide(5, 3, success);
	if (!success)
		cerr << "An error occurred" << endl;
	else
		cout << "Result is " << result << endl;
	ifstream input_file("temp.txt");
	if(!input_file)
		cerr << "Cannot open file" << endl;
	return 0;
}

 

위의 문법대로 예전에는 많이 했다(C style)

예외처리가 문법적으로 좋아 보이지만, 정말로 예측할 수 없는 일이 빈번하게 발생하는 경우 많이 쓰인다.

예외 처리의 속도가 일반적인 오류 처리 방법보다 느리기 때문이다.

 

#include <iostream>
#include <string>

using namespace std;


int main()
{
	//try, catch, throw
	//try를 해서 아무 일도 없다면 계속 작동
	//만약 이상한 일이 생기면 throw로 예외를 던진다
	//catch는 던저진 에러를 잡아서 처리한다.
	double x;
	cin >> x;
	try
	{
		if (x < 0.0) throw string("Negative input");
		cout << sqrt(x) << endl;
	}
	catch (string error_message)
	{
		cout << error_message << endl;
	}
	return 0;
}

 

 

//예외 처리는 엄격하다. 형 변환 같은 경우에서도 그렇다.

위의 경우 error_message가 string으로 들어가기 때문에, 그냥 const char*로 하면 받지를 못한다.

 

try
	{
		if (x < 0.0) throw string("Negative input");
		cout << sqrt(x) << endl;
	}
	catch (string error_message)
	{
		cout << error_message << endl;
	}
	catch (int x)
	{
		cout << "Catch Integer " << x << endl;
	}
	catch (const char* error_message)
	{
		cout << "Char * " << error_message << endl;
	}

throw하는 타입에 맞춰 catch 처리를 할 수 있다.

type이 맞지 않을 경우 OS가 런타임 에러를 경고한다.

 

 

 

14.2 예외처리와 스택 되감기Satck Unwinding

 

 

#include <iostream>
#include <string>

using namespace std;

void last()
{
	cout << "last " << endl;
	cout << "Throws exception" << endl;
	throw - 1;
	cout << "End last " << endl;
}
void third()
{
	cout << "Third" << endl;
	last();
	cout << "End third " << endl;
}

void second()
{
	cout << "Second" << endl;
	try
	{
		third();
	}
	catch (double)
	{
		cerr << "Second caught double exception" << endl;
	}
	cout << "End second " << endl;
}

void first()
{
	cout << "first" << endl;
	try
	{
		second();
	}
	catch (int)
	{
		cerr << "first caught int exception" << endl;
	}
	cout << "End first " << endl;
}

int main()
{
	cout << "Start" << endl;
	try
	{
		first();
	}
	catch (int)
	{
		//cerr는 에러 메시지, 버퍼를 거치지 않고 빠르게 출력.
		//clog
		cerr << "main caught int exception" << endl;
	}
	cout << "End Main" << endl;
	return 0;
}

 

	cout << "Start" << endl;
	try
	{
		first();
	}
	catch (int)
	{
		//cerr는 에러 메시지, 버퍼를 거치지 않고 빠르게 출력.
		//clog
		cerr << "main caught int exception" << endl;
	}
	catch (...) //catch-all handlers
	{
		cerr << "main caught ellipses exception" << endl;
	}

나머지 반환형의 catch에 대해선 ellipses를 사용할 수 있다.

 

void last() throw(int)

라고 함수를 선언할 경우 int형 에러를 throw할 수 있다는 말이다.

(필요가 없다는 견해가 더 많다)

 

void last() throw() 이렇게 걸면, 함수가 아무것도 예외를 던지지 않는다는 뜻

void last() throw(...) 이렇게 걸면, 어떠한 반환형의 throw이든지 던지겠다는 뜻

 

 

 

14.3 예외 클래스와 상속

#include <iostream>
#include <string>

using namespace std;
class MyArray
{
private:
	int m_data[5];
public:
	int& operator[] (const int& index)
	{
		if (index < 0 || index >= 5) throw - 1;
		return m_data[index];
	}
};

void doSomething()
{
	MyArray my_array;
	try
	{
		my_array[100];
	}
	catch (const int& x)
	{
		cerr << "Exception " << x << endl;
	}
}
int main()
{
	doSomething();
	return 0;
}

 

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public:
	void report()
	{
		cerr << "Exception report" << endl;
	}
};

class MyArray
{
private:
	int m_data[5];
public:
	int& operator[] (const int& index)
	{
		if (index < 0 || index >= 5) throw Exception();
		return m_data[index];
	}
};

void doSomething()
{
	MyArray my_array;
	try
	{
		my_array[100];
	}
	catch (const int& x)
	{
		cerr << "Exception " << x << endl;
	}
	catch (Exception& e)
	{
		e.report();
	}
}
int main()
{
	doSomething();
	return 0;
}

 

예외 클래스를 새로 정의할 수 있다.

 

 

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public:
	void report()
	{
		cerr << "Exception report" << endl;
	}
};

class ArrayException : public Exception
{
public:
	void report()  //overriding
	{
		cerr << "Array exception" << endl;
	}
};
class MyArray
{
private:
	int m_data[5];
public:
	int& operator[] (const int& index)
	{
		if (index < 0 || index >= 5) throw ArrayException();
		return m_data[index];
	}
};

void doSomething()
{
	MyArray my_array;
	try
	{
		my_array[100];
	}
	catch (const int& x)
	{
		cerr << "Exception " << x << endl;
	}
	catch (Exception& e)
	{
		e.report();
	}
	catch (ArrayException& e)
	{
		e.report();
	}
}
int main()
{
	doSomething();
	return 0;
}

 

위의 경우, doSomething()에서 ArrayException을 던지고 있지만

catch에서 Exception(부모 클래스)가 먼저 있으므로 ArrayException은 Exception에 걸리고 만다.

이를 해결하기 위해선 catch(Exception & e)보다 catch(ArrayException & e)를 먼저 쓰기만 하면 된다.

 

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public:
	void report()
	{
		cerr << "Exception report" << endl;
	}
};

class ArrayException : public Exception
{
public:
	void report()  //overriding
	{
		cerr << "Array exception" << endl;
	}
};
class MyArray
{
private:
	int m_data[5];
public:
	int& operator[] (const int& index)
	{
		if (index < 0 || index >= 5) throw ArrayException();
		return m_data[index];
	}
};

void doSomething()
{
	MyArray my_array;
	try
	{
		my_array[100];
	}
	catch (const int& x)
	{
		cerr << "Exception " << x << endl;
	}
	catch (ArrayException& e)
	{
		e.report();
	}
	catch (Exception& e)
	{
		e.report();
	}
	
}
int main()
{
	try
	{
		doSomething();
	}
	catch (ArrayException& e)
	{
		cout << "main()" << endl;
		e.report();
	}
	return 0;
}

 

이 경우 main에서는 단연 에러가 잡히지 않는다.

그러나 다시 에러를 던져야 하는 경우, re-throw를 할 수 있다.

 

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public:
	void report()
	{
		cerr << "Exception report" << endl;
	}
};

class ArrayException : public Exception
{
public:
	void report()  //overriding
	{
		cerr << "Array exception" << endl;
	}
};
class MyArray
{
private:
	int m_data[5];
public:
	int& operator[] (const int& index)
	{
		if (index < 0 || index >= 5) throw ArrayException();
		return m_data[index];
	}
};

void doSomething()
{
	MyArray my_array;
	try
	{
		my_array[100];
	}
	catch (const int& x)
	{
		cerr << "Exception " << x << endl;
	}
	catch (ArrayException& e)
	{
		cout << "doSomething()" << endl;
		e.report();
		throw e;
	}
	catch (Exception& e)
	{
		e.report();
	}
	
}
int main()
{
	try
	{
		doSomething();
	}
	catch (ArrayException& e)
	{
		cout << "main()" << endl;
		e.report();
	}
	return 0;
}

함수에 대해 rewinding이 되므로, something 함수의 밑 쪽 catch가 아닌,

something 함수를 부른 main에서 에러를 다시 catch할 수 있다.

 

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public:
	void report()
	{
		cerr << "Exception report" << endl;
	}
};

class ArrayException : public Exception
{
public:
	void report()  //overriding
	{
		cerr << "Array exception" << endl;
	}
};
class MyArray
{
private:
	int m_data[5];
public:
	int& operator[] (const int& index)
	{
		if (index < 0 || index >= 5) throw ArrayException();
		return m_data[index];
	}
};

void doSomething()
{
	MyArray my_array;
	try
	{
		my_array[100];
	}
	catch (const int& x)
	{
		cerr << "Exception " << x << endl;
	}
	catch (Exception& e)
	{
		cout << "in doSomething()" << endl;
		e.report();
		throw e;
	}
	
}
int main()
{
	try
	{
		doSomething();
	}
	catch (Exception& e)
	{
		cout << "main()" << endl;
		e.report();
	}
	catch (ArrayException& e)
	{
		cout << "main()" << endl;
		e.report();
	}
	return 0;
}

이렇게 exception을 거친 후 다시 throw e를 하면 main에 와서도 원래형 "ArrayException"이 아닌 그냥 "Exception"으로 받는다.

이 때 throw e;가 아닌 throw; 만 하면 외부에서는 원래의 에러형 " ArrayException"으로 받을 수 있게 된다!

 

 

14.4 std::exception 소개

 

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;


int main()
{
	try
	{
		string s;
		s.resize(-1);

		
	}
	catch (exception& exception)
	{
		cerr << exception.what() << endl;
		//what() 무슨 에러인지.
	}
	return 0;
}

 

exception 클래스는 자식을 많이 가지고 있다.

typeid(exception).name()

처럼 사용하면, 어떤 에러가 생겼는지 그 자식 클래스의 이름을 자세히 알 수 있다.

 

https://en.cppreference.com/w/

 

cppreference.com

Null-terminated strings:    byte  −   multibyte  −   wide

en.cppreference.com

위 페이지에서 메소드를 찾고, 해당 메소드에서 어떤 예외가 던져질 수 있는지 확인할 수도 있다.

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;


int main()
{
	try
	{
		/*string s;
		s.resize(-1);*/
		throw runtime_error("Bad thing happened");
		
	}
	catch (exception& exception)
	{
		cout << typeid(exception).name() << endl;
		cerr << exception.what() << endl;
		//what() 무슨 에러인지.
	}
	return 0;
}

 

직접 except 클래스의 자식 클래스들 중 하나를 에러로 던질 수 있다.

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;

class CustomException : public exception
{
public:
	const char* what() const noexcept override
		//noexcept : 적어도 이 안에선 exception을 던지지 않겠다.
	{
		return "Custom exception";
	}
};

int main()
{
	try
	{
		/*string s;
		s.resize(-1);*/
		throw CustomException();
		
	}
	catch (exception& exception)
	{
		cout << typeid(exception).name() << endl;
		cerr << exception.what() << endl;
		//what() 무슨 에러인지.
	}
	return 0;
}

exception 클래스를 상속받아 what()을 오버라이딩을 할 수 있다.

 

14.5 함수 try

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;

void doSomething()
try
{
	throw - 1;
}
catch (...)
{
	cout << "Catch in doSomething()" << endl;
}

int main()
{
	try
	{
		
		doSomething();
	}
	catch (...)
	{
		cout << "Catch in main()" << endl;
	}
	return 0;
}

 

 

함수에 바로 try-catch를 사용할 수 있지만 많이 사용되진 않는다.

 

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;

class A
{
private:
	int m_x;
public:
	A(int x) : m_x(x)
	{
		if (x <= 0)
			throw 1;
	}
};

class B : public A
{
public:
	B(int x)
		: A(x)
	{

	}
};

int main()
{
	try
	{
		
		B b(0);
	}
	catch (...)
	{
		cout << "Catch in main()" << endl;
	}
	return 0;
}

 

이렇게 하면 물론 메인에서 예외를 잡는다.

가끔 생성자에서 에러를 잡고 싶은 경우.

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;

class A
{
private:
	int m_x;
public:
	A(int x) : m_x(x)
	{
		if (x <= 0)
			throw 1;
	}
};

class B : public A
{
public:
	B(int x) try : A(x)
	{
		//do initialization
	}
	catch (...)
	{
		cout << "Catch in B constructor " << endl;
		//throw;
	}
};

int main()
{
	try
	{
		
		B b(0);
	}
	catch (...)
	{
		cout << "Catch in main()" << endl;
	}
	return 0;
}

이 때 생성자에서 예외 처리를 하면, throw를 한 것처럼 메인에서도 한 번 더 잡는다.

(일반적인 함수에선 저렇게 진행되진 않는다.)

 

 

14.6 예외처리의 위험성과 단점

 

#include <iostream>
#include <string>
#include <exception>
using namespace std;


int main()
{
	try
	{
		int* i = new int[10000];
		//do something with i
		throw "error";
		delete[] i;
	}
	catch (...)
	{
		cout << "catch" << endl;
	}
	return 0;
}

위 코드처럼 사용할 경우, i에서 예외가 발생한다면, delete가 실행되지 않고 예외가 처리되어

메모리 누수가 발생할 수 있다.

#include <iostream>
#include <memory>
using namespace std;


int main()
{
	try
	{
		int* i = new int[10000];
		unique_ptr<int> up_i(i);
		//do something with i
		throw "error";
		//delete[] i;
	}
	catch (...)
	{
		cout << "catch" << endl;
	}
	return 0;
}

이것을 해결할 수 있는 방법 중에 library <memory>에 들어 있는 unique_ptr이 유용하다.

delete가 없어도 알아서 지워주고, 예외가 발생해도 잘 처리해준다.

 

#include <iostream>
#include <memory>
using namespace std;

class A
{
public:
	~A()
	{
		throw "error";
	}
};
int main()
{
	try
	{
		A a;
	}
	catch (...)
	{
		cout << "catch" << endl;
	}
	return 0;
}

Destructor에선 예외를 못 던지게 막고 있다.