예외 클래스( exception class )
throw는 예외를 발생시키기 위해서 어떠한 타입의 값이라도 사용할 수 있고, 이런 타입의 종류에는 당연히 클래스도 포함됩니다.
[C++] 예외 처리를 위한 throw와 try-catch의 동작 방식
C++에서의 예외( exception ) 처리C++에서는 프로그램의 오류나 기대하지 못했던 상황이 발생한 경우, 이를 처리하기 위한 예외( exception )를 발생시키면, 프로그램이 자동으로, 이 예외를 처리할 수
codingbonfire.tistory.com
예외를 발생시킬 때 사용되는 클래스를 예외 클래스( exceptin class )라고 부르는데, 사실 이 예외 클래스와 일반 클래스의 차이점은 전혀 없습니다.
단지, 예외가 발생된 이유를 설명하기 위한 값과 함수를 제공한다는 차이가 있을 뿐입니다.
#include <iostream>
#include <string>
using namespace std;
class BaseException{ // 사용자 정의 예외 클래스
string m_strReason;
public:
BaseException( const string& str) : m_strReason(str){}
// 예외를 설명하기 위한 const 멤버 함수
const string& getReason() const{
return m_strReason;
}
};
int main(){
try{
BaseException e("My Exception occurred");
throw e;
}
catch( const BaseException& e){ // 이 종류의 예외는 따로 처리
cout << e.getReason() << '\n';
}
catch(...){ // 나머지 타입의 예외 처리
// do something...
}
}
▼출력
My Exception occurred
하지만, 예외를 처리할 때, 예외 클래스를 사용하는 것은 몇 가지 장점이 있습니다.
● 첫 번째는, 더 많은 정보를 묶어서 쉽게 사용할 수 있다는 점입니다.
위의 예문에서는 BaseException
클래스가 문자열 정보만 갖고 있지만, 이러한 현재의 설계는 나중에라도 쉽게 변경할 수 있습니다.
만약 예외가 발생할 때, std::string
을 통해서 정보를 보내왔다고 한다면, 추후 추가 정보를 보내기 위해선 많은 부분을 변경해야 할 것입니다.
● 두 번째는, 예외의 사용자 타입( user defined type )을 만들 수 있다는 점입니다.
이 타입의 예외는 int
같은 타입의 예외나, 다른 라이브러리에서 발생시키는 예외와 구분해서 처리할 수 있습니다.
위의 예문에서도, BaseException
타입의 예외를 다른 타입의 예외들과 구분할 수 있습니다.
이렇게 되면, 특정한 예외에 따른 특정한 동작을 만들어 낼 수 있게 됩니다.
● 세 번째는 클래스 상속 설계를 통해, 위의 두 가지 장점을 모두 지니면서도 쉽게 확장할 수 있다는 점입니다.
또한, 인터페이스 설계를 통해, 파생된 예외 클래스가 예상가능하고 일관적인 동작을 취하도록 구현할 수 있습니다.
이러한 예외 클래스 객체는, catch 블록으로 전달될 때, 일반 함수 호출 시 인자를 전달하는 방식과 마찬가지로 처리됩니다.
따라서, 문자열을 전달받는 함수는 일반적으로 const string&
타입의 매개 변수를 사용하는 것과 같이, 아래와 같이, 불필요한 복사가 발생하지 않도록, 참조( reference ) 타입의 매개 변수를 사용하는 것이 좋습니다.
try{
// do something...
}
catch( const BaseException& e){ // 불필요한 복사를 방지하기 위해
cout << e.getReason() << endl;
}
예외 클래스의 상속
위의 BaseException
을 상속받은 DerivedException
을 추가하고, DerivedException
타입의 예외를 발생시켰습니다.
그럼, 다음 코드는 어떻게 실행될까요?
class BaseException{ // 사용자 정의 예외 클래스
string m_strReason;
public:
BaseException( const string& str) : m_strReason(str){}
// 예외를 설명하기 위한 const 멤버 함수
const string& getReason() const{
return m_strReason;
}
};
// BaseException에서 파생된 클래스
class DerivedException : public BaseException{
public:
DerivedException( const string& str): BaseException(str){}
};
int main(){
try{
DerivedException e("Derived");
throw e;
}
catch( const BaseException& e){
cout << "BaseException occurred\n";
}
catch( const DerivedException& e){
cout << "DerivedException occurred\n";
}
}
▼출력
BaseException occurred
위의 경우는, 파생된 클래스 객체를, 부모 클래스 타입의 참조( BaseException&
)를 매개 변수로 하는 함수에 전달할 수 있는 것과 마찬가지입니다. DerivedException
객체는 BaseException
타입으로 볼 수 있으므로, 예외를 처리하는 catch( const BaseException& e )
블록을 검사할 때 자연스럽게 통과됩니다.
그래서, 그다음에 위치한 catch( const DerivedException& e )
블록은 검사조차 되지 않습니다.
이것은, if 구문에서 &&
연산자 앞의 값이 true
면, 연산자 뒤의 값을 평가하지 않는 것과 같습니다.
따라서, DerivedException
타입의 처리기가 호출되게 하려면, 이 파생된 클래스 타입의 catch 블록이, 부모 클래스 타입의 catch 블록보다 상위에 위치해야 합니다.
int main(){
try{
DerivedException e("Derived");
throw e;
}
catch( const DerivedException& e){ // 부모 클래스 타입보다 위에 위치
cout << "DerivedException occurred\n";
}
catch( const BaseException& e){
cout << "BaseException occurred\n";
}
}
▼출력
DerivedException occurred
이제, DerivedException
타입의 예외는 DerivedException
처리기에서, BaseException
타입의 예외는 BaseException
처리기에서 각각 처리할 수 있게 되었습니다.
예외 객체의 복사
보통 예외 객체는 예외를 발생시키는 함수의 지역 변수인 경우가 많습니다.
void func(){
BaseException e; // 지역 변수
throw( e );
}
그런데, 예외가 발생되면, 스택 언와인딩( stack unwinding )이 실행되기 때문에, 함수 내의 모든 지역 변수들은 파괴됩니다.
그래서, 컴파일러는, 명시되지 않은 메모리에, 이 예외 객체의 복사본을 작성하고, 이렇게 복사된 예외 객체는 예외가 처리가 끝날 때까지 계속 보존됩니다.
그러므로, 예외 객체는 복사가 가능한 상태이어야 합니다.
// 복사할 수 없는 예외 클래스
class UncopyableException{
public:
UncopyableException() = default;
// 복사 생성자를 삭제
UncopyableException( const UncopyableException& other) = delete;
};
int main(){
try{
UncopyableException e;
throw e; // compile error !!
}
catch( ...){
cout << "Exception occurred\n";
}
}
위의, 예외 클래스인 UncopyableException
클래스는 복사 생성자를 삭제했습니다.
그래서, 컴파일러는 이 복사할 수 없는 예외 객체에 대해 오류를 발생합니다.
그리고, 이와 같은 이유 때문에, 예외 클래스( exception class )는 ( 스택에 저장되는 ) 지역 변수를 가리키는 포인터나, 참조 변수를 가져서는 안 됩니다.
이 변수들이 참조하는 대상들은, 예외가 발생되면, 모두 파괴될 것이기 때문입니다.
std::exception
C++ 표준 라이브러리를 사용하다 보면, 수많은 클래스에서 예외( exception )를 발생시키는 것을 알게 됩니다.
그런데, 이 예외를 막상 처리하려고 하면, 의외의 장애물을 만나게 됩니다.
바로, 예외의 타입입니다.
표준 라이브러리에는 다양한 예외 상황을 알리기 위해, 수많은 예외 클래스를 만들어 두었고, 예외가 발생하면 그때그때 상황에 적합한 예외 클래스 타입의 객체를 던집니다.
그러나, 이러한 예외 클래스들에 익숙하지 않으면, 어떤 예외 클래스 타입을 사용해야 하는지 모르므로, 참조 문서나 코드를 들여다봐야 하는 문제가 발생하는 것입니다.
그렇지만 다행인 것은, 표준 라이브러리 사용 시, 다음의 내용을 예외 처리의 시작으로 삼을 수 있습니다.
std::exception은 C++ 표준 라이브러리의 모든 예외 클래스들의 루트( root ) 클래스입니다.
그래서, std::vector
클래스를 쓰면서 예외가 발생하더라도, 실제로 어떤 타입의 예외를 발생시키는지를 반드시 알 필요는 없습니다.
#include <iostream>
#include <vector>
#include <limits> // for numeric_limits
using namespace std;
int main(){
vector<int> vec;
try{
// 메모리 부족을 발생시키도록 커다란 값을 사용
vec.resize( numeric_limits<size_t>::max());
}
catch( const exception& e ){
cout << "standard error: " << e.what() << endl;
}
}
▼출력
standard error: vector::_M_default_append
위의 예에서, 예외를 일부러 발생시키기 위해, 커다란 숫자의 메모리를 요청하는 코드를 사용했습니다.
하지만, std::exception
타입의 예외를 처리하는 catch 블록으로, C++ 표준 라이브러리에서 발생한 모든 예외를 잡아낼 수 있습니다.
( 환경에 따라, 위의 출력되는 메시지는 다를 수 있습니다 )
물론, 예외 타입에 따른 좀 더 세밀한 처리 방법이 필요하다면, std::vector
클래스가 발생시키는 예외를 알기 위한 정보를 찾아보아야 합니다.
위 같은 경우, std::length_error
타입의 예외를 발생시킵니다.
( 참고로, 여기에서 C++ 표준 라이브러리가 발생시키는 예외에 관한 정보를 볼 수 있습니다. )
그래서 아래와 같이, 필요한 겨우 length_error
타입의 예외만 따로 처리할 수도 있습니다.
int main(){
vector<int> vec;
try{
// 메모리 부족을 발생시키도록 커다란 값을 사용
vec.resize( numeric_limits<size_t>::max());
}
catch( const length_error& e){
cout << "length_error: " << e.what() << endl;
}
catch( const exception& e ){
cout << "standard error: " << e.what() << endl;
}
}
▼출력
length_error: vector::_M_default_append
● 반대의 경우도 있습니다.
표준 라이브러리의 예외 클래스를 이용해서, 간단히 예외를 발생시키고 싶을 때입니다.
이 경우엔 std::exception
객체를 사용할 수 없습니다. std::exception
클래스는 예외 클래스들의 추상 클래스( abstract class )이기 때문입니다.
[C++] 추상 클래스( abstract class )와 순수 가상 소멸자
추상 클래스와 순수 가상 함수( pure virtual function )복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 추상화라고 합니다.C++에서는 주로 클래스를 통해
codingbonfire.tistory.com
이럴 때 가장 사용하기 간편한 예외 클래스는 std::runtime_error
입니다.
이 클래스의 이름이 포괄적인 예외 상황을 표현하고, 이 클래스의 생성자는 예외의 원인을 알 수 있는 문자열을 인수로 받기 때문입니다.
int main(){
try{
std::runtime_error e( "generic error occurred");
throw e;
}
catch( const exception& e){
cerr << e.what() << endl;
}
}
▼출력
generic error occurred
std::exception에서 파생된 사용자 예외 클래스
다른 표준 라이브러리 클래스들과 마찬가지로, std::exception
클래스에서 상속받은 사용자( user defined ) 예외 클래스를 만들 수 있습니다.
이 std::exception
클래스는 가상 함수인 what 만을 제공하는데, 이 함수는 발생한 예외에 관한 텍스트 정보를 제공하는 역할을 합니다.
virtual const char *what() const noexcept;
아래는, 이 함수를 재정의( override )한 사용자 예외 클래스에 예문입니다.
// std::exception 으로부터 상속받은 사용자 예외 클래스
class MyException : public exception{
string m_err;
public:
MyException( const string err): m_err(err) {}
const char* what() const noexcept{ // 재정의한 멤버 함수
return m_err.c_str();
}
};
int main(){
vector<int> vec;
try{
MyException e("MyException occurred"); // 사용자 예외 클래스
throw e;
}
catch( const exception& e ){
cout << e.what() << endl;
}
}
▼출력
MyException occurred
위에 재정의된 what 함수는 const 속성 외에, noexcept 속성을 가진 함수입니다.
[C++] 예외가 발생되지 않는 함수를 선언하는 noexcept
noexcept 지시자( specifier )noexcept는 함수 이름의 끝에 붙는 지시자로, 이 지시자가 붙은 함수는 예외( exception )를 발생시키지 않는다는 것을 명시합니다. 하지만, noexcept 함수 실행 시, 컴파일러에
codingbonfire.tistory.com
이 noexcept 함수는 오버로딩( overloading ) 할 수 없으므로, MyException
클래스에서 what 함수를 정의할 때도, noexcept 지시자를 사용해야 합니다.
정리
- 예외를 처리하는 데 사용되는 클래스를 예외 클래스( exception class )라고 합니다.
- 예외 발생 시, 지역 변수인 예외 객체는 복사되고, 예외 처리가 끝날 때까지 저장됩니다.
std::exception
은 C++ 표준 라이브러리 예외 클래스들의 최상위 클래스입니다.std::runtime_error
클래스는, 예외를 발생하고자 할 때 쉽게 사용할 수 있는, 표준 라이브러리의 예외 클래스입니다.
이 글과 관련 있는 글들