C & C++/C++
[C++] 예외 처리
거북이 코딩
2024. 8. 8. 16:50
Ch 05. 예외 처리
전통적인 예외 처리
예외가 발생했을 때 사용할 수 있는 방법은 여러 가지가 있다.
// 프로그램을 종료하는 방법
std::abort(); // 비 정상 종료
exit(123); // 코드와 함께 종료
return 123; // 코드와 함께 종료
// 반환하여 오류를 알려주는 방법
printf(); // 에러가 나면 -1 반환, 정상이면 글자 수 반환
std::cout<<10;
cout.fail(); // 에러가 나면 true 반환
이외에도 전역변수로 errorCode 확인하기 등 방법이 있지만 예외처리가 강제되지 않기 때문에 예외가 무시될 수 있다. 예외처리를 강제라기 위한 메커니즘으로 C++에서 try catch를 사용한다.
#include <iostream>
using std::cout;
using std::endl;
int divide(double d, double v, double& result) // 결과를 다른 변수에 저장하기
{
if (v == 0)
{
result = 0;
return -1;
}
result = d / v;
return 0;
}
int errorCode = 0;
double divide(double d, double v) // 전역 변수에 에러 여부 저장하기
{
if (v == 0)
{
errorCode = -1;
return 0;
}
errorCode = 0;
return d / v;
}
int main()
{
// 아래의 예외 처리 방법은 예외 처리가 강제 되지 않는다.
// printf 가 실패했지만 실패한 것에 대한 예외 상황을 처리하지 않고 작성하더라도 눈치채지 못할 수 있다.
// 따라서 실패 시 실패에 대한 처리를 강제하게 하는 메커니즘이 필요
double result;
int errorCode = divide(10, 0, result); // 에러 발생 시 에러 코드
if (errorCode > 0)
cout << result << endl;
double result = divide(10, 0);
if (errorCode > 0) // 전역적으로 사용하는 변수, 멀티 쓰레드에서 문제 발생
// 에러 처리
}
try catch
예외가 발생했을때 throw를 사용한다.
throw "Divide by zero";
// throw가 발생하면 내부적으로 std::terminate(); 가 호출된다.
std::terminate(); // teminate는 abort와 다르게 수정할 수 있다.
void terminateFunc()
{
cout << "terminate" << endl;
}
std::set_terminate(terminateFunc);
try 블록내에서 throw 된다면 catch 블록에서 잡아서 예외를 처리해 줄 수 있다.
#include <iostream>
using std::cout;
using std::endl;
double divide(double d, double v)
{
if (v == 0)
{
throw "Dirived by zero"; // 처리 되지 않으면 std::terminate
}
return d / v;
}
int main()
{
try
{
cout << divide(10, 0) << endl;
}
catch (const char* e) // 처리할 예외의 타입
{
cout << e << endl;
}
catch (...) // 처리할 예외타입이 무엇인지 알지 못할 떄
{
cout << "..." << endl;
}
}
상속 관계에 있는 객체를 던질 경우
class Parent {};
class Child : public Parent {};
int main()
{
try
{
throw Child();
}
catch (Parent&) // 부모 타입으로 받을 수 있다
{
cout << "Parent" << endl;
}
catch (Child&)
// catch 순서대로 체크를하기 때문에 위쪽 catch에 호환이 되기 때문에 위쪽 catch로 가게 된다.
{
cout << "Child" << endl;
}
}
stack unwinding 스택 풀기 : throw가 되면 catch 블록을 만날 때까지 stack을 제거한다.
class Test
{
public:
~Test() { cout << "test" << endl; }
};
void func1()
{
Test t; // throw되면 자동 지역 변수는 모두 파괴 된다.
divide(10, 0);
// throw로 인해 이 줄은 실행되지 않는다.
}
void func0()
{
Test t;
func1();
// throw로 인해 이 줄은 실행되지 않는다.
}
int main()
{
try
{
func0();
// divide에서 예외가 발생했지만 예외를 처리할 수 있는 곳까지 함수 스택을 되돌린다
}
catch (const char* e)
{
cout << e << endl;
throw; // 받은 예외를 rethrow 할 수 있다. 그대로 다시 던진다.
}
}
함수 자체에 try를 거는 경우
// 함수 자체에 try를 걸 수도 있다
void foo() try
{
}
catch (const char* e)
{
}
std::exception 을 상속해서 예외를 만들면 좋다
// std::exception을 상속해서 예외 객체를 만들자
// 표준 라이브러리의 예외들도 std::exception을 상속했음
std::runtime_error; // 표준 라이브러리의 예외 중 하나
class CustomException : public std::exception
{
public:
virtual const char* what() const override
{
return "Custom";
}
}
//std::exception 의 what()을 오버라이드 해서 무슨 예외인지 표기한다.
RAII
Stack Unwinding, 예외가 발생했을 때 스택을 파괴하는 과정에서 동적 할당된 객체가 있을 경우 메모리가 정상적으로 해제되지 않는다. 이러한 문제를 해결하기 위해 RAII를 적용한다.
#include <iostream>
using std::cout;
using std::endl;
class Test
{
public:
~Test() { cout << "~Test" << endl; }
};
void func()
{
throw "exception";
}
int main()
{
try
{
int* i = new int; // 메모리 누수 발생
func();
}
catch (const char* e)
{
cout << e << endl;
}
}
RAII를 사용하지 않을 경우 해결 방법
int main()
{
int* i = nullptr
try
{
i = new int;
func();
}
catch (const char* e)
{
delete i; // catch가 여러개일 경우 일일히 해주어야하고 객체가 여러개일 경우
// 매우 불편해진다.
cout << e << endl;
}
}
RAII : Resource Acquisition Is Initialization 기법을 사용하는 방법
#include <memory>
class RAII
{
public:
int* i;
RAII() // 생성 시 동적할당
{
i = new int;
}
~RAII() // 파괴시 자동으로 메모리를 해제
{
cout << "~RAII" << endl;
delete i;
}
};
class Test
{
public:
~Test() { cout << "~Test" << endl; }
};
void func()
{
throw "exception";
}
int main()
{
try
{
RAII raii;
func();
} // 블럭을 벗어나면서 RAII의 소멸자가 호출되면서 내부에 할당한 동적 객체가 해제된다
catch (const char* e)
{
cout << e << endl;
}
try
{
// unique_ptr을 이용해 동적할당에 대한 RAII를 사용할 수 있다
std::unique_ptr<Test> test(new Test()); // memory를 include
func();
}
catch (const char* e)
{
cout << e << endl;
}
}
unique_ptr을 사용하면 지역을 벗어나며 자동으로 메모리가 해제된다.
noexcept
예외가 발생하지 않는다는 의미이다. 컴파일러에게 이 사실을 알려주어 좀 더 최적화를 할 수 있다.
int main noexcept
{
return 0;
}
void func0() noexcept(true) {
// 이 함수도 예외를 던지지 않습니다.
}
void func1() noexcept(false) {
// 이 함수는 예외를 던질 수 있습니다.
}
noexcept(bool) bool에 다른 조건식을 넣어 조건부 noexcept를 선언할 수 있다.
#include <iostream>
using std::cout;
using std::endl;
// 소멸자는 기본적으로 noexcept 함수
void func() noexcept // 해당 함수에서는 예외가 나지 않는다고 선언, 컴파일러가 최적화 할 수 있다
{
//throw 1;
// noexcept 선언한 함수에서 throw 된다고 하더라도 try catch로 처리할 수 없다
}
int main()
{
try
{
func();
}
catch (int e)
{
}
// 몇몇 STL 함수에서는 noexcept로 지정된 함수가 제공되지 않는 경우 성능 손실이 생길 수 있다
}
noexcept 의 함수 포인터
using FuncType = void (*)() noexcept;
void safeFunction() noexcept {
// 안전한 함수
}
FuncType f = safeFunction;
noexcept 선언된 함수 포인터에는 noexcept 함수만 올 수 있다.
선언 되지 않은 함수에는 noexcept 유무에 관계 없이 함수가 올 수 있다.