나에게 가장 익숙한 예외처리 메커니즘은 if문을 사용한 예외처리였다. 하지만 Effective C++, STL과 관련된 책들을 보며 공부할 때 자주 등장하는 문법이 'try ~ catch' 였다. 찾아보니 C++에서 예외 처리의 구현을 위해서 try, catch, throw를 제공한다고 한다. 이 문법을 사용하면 가독성과 유지보수성을 높일수 있고
예외 처리를 프로그램의 흐름에서 독립시킬 수 있다.
예외(exception)란 컴퓨터 시스템이 동작하는 도중에 예상하지 못한 오류가 발생하여, 실행되고 있던 프로그램이 중지되는 것을 의미합니다.
예외 처리(exception handling)는 이러한 예외 상황을 처리할 수 있도록 코드의 흐름을 바꾸는 행위를 의미합니다.
C++은 언어 차원에서 예외 처리 문법을 제공하여, 예외 처리하는 방식을 확장하고 관리하기 쉽도록 해줍니다.
출처 - TCP School
try 블록은 예외발생에 대한 검사 범위를 지정한다. try 블록에서 예외가 발생하면 catch 블록으로 넘어가 예외를 처리하는 방식이다. throw 에서는 예외가 발생했음을 알린다.
즉, try 블록으로 검사 했을 때 발생한 예외를 throw로 전달해주고 catch 블록에 의해 처리된다.
int main() {
int num1 = 0, num2 = 0;
std::cout << "두 개의 정수를 입력하세요. : ";
std::cin >> num1 >> num2;
try {
if (num2 == 0)
throw num2;
std::cout << "나눗셈의 몫 : " << num1 / num2 << "\n";
std::cout << "나눗셈의 나머지 : " << num1 % num2 << std::endl;
}
catch (int exception) {
std::cout << exception << " 으로 나눌 수 없습니다." << std::endl;
}
return 0;
}
두 숫자(피제수, 제수)를 입력하면 두 숫자를 나눠 몫과 나머지를 출력하는 간단한 코드이다. 근데 당연하게도 제수는 0이 될 수 없다. 그렇다면, 제수에 0이 입력됐을 때 예외가 발생하기에 예외 처리를 작성해야한다.
- try문에 도달한 프로그램은 try문 내의 코드를 실행한다.
- 이때 예외가 발생하지 않으면 프로그램은 마지막 catch 절 바로 다음으로 이동한다.
- 만약 예외가 발생(throw)하면 catch 핸들러는 아래의 순서로 적절한 catch 절을 찾는다.
3-1. 스택에서 try문과 가장 가까운 catch 절부터 차례대로 검사한다.
3-2. 만약 적절한 catch 절을 찾지 못하면 다음 바깥쪽 try 문의 catch절을 차례대로 검사한다.
3-3. 가장 바깥쪽 try 문까지 계속 검사를 진행한다.
3-4. 적절한 catch 절을 찾지못하면, 미리 정의된 terminate() 함수를 호출한다.- 적절한 catch 절을 찾으면, throw 문의 피연산자는 예외 객체의 형식 매개변수로 전달된다.
- 모든 예외 처리가 끝나면 프로그램은 마지막 catch 절 바로 다음으로 이동한다.
3-4의 terminate()는 기본적으로 abort를 호출한다고 한다. Effective C++에서 set_terminate() 함수를 통해 abort 대신에 새로운 함수를 호출할 수 있다. 라고 본 기억이 있다. 나중에 한번 찾아볼 생각이다.
예외 메커니즘 중 3-1 ~ 3-4의 과정을 스택 풀기라 칭한다고 한다.
스택 풀기란 "예외를 처리하는 영역을 찾지 못해서 해당 예외가 호출된 영역의 상위 함수로 예외가 계속해서 전달되는 현상"이라고 한다.
void Function_C() {
std::cerr << "Function C!\n";
throw -1;
}
void Function_B() {
std::cerr << "Function B!\n";
Function_C();
}
void Function_A() {
std::cerr << "Function A!\n";
Function_B();
}
int main() {
try {
Function_A();
}
catch (int exception) {
std::cout << "예외 처리(main) : " << exception << std::endl;
}
return 0;
}
위의 스택 풀기 코드에서는 main -> A -> B -> C 순으로 함수를 호출한다. Function_C()에서 예외가 발생(throw)하는데 Function_C()에서는 발생한 예외를 처리할 catch 절이 없기 때문에 Function_B() 함수로 예외를 전달한다. 이때 스택에 저장되어 있던 Function_C() 함수에 관한 스택 프레임을 모두 pop하고 Function_B()로 이동한다. 또 Function_B()에서 처리할 catch 절이 없기 때문에 스택에 저장된 Function_B()에 관한 스택 프레임을 모두 pop하고 Function_A() ~~~..
결국, main() 함수 내의 catch 절에서 수행하게 된다는 것이 스택 풀기이다.
만약, throw하는 자료형과 catch에서 받는 자료형이 다르면 어떻게 될까?
void Function_C() {
std::cerr << "Function C!\n";
throw 1.7;
}
void Function_B() {
std::cerr << "Function B!\n";
Function_C();
}
void Function_A() {
std::cerr << "Function A!\n";
Function_B();
}
int main() {
try {
Function_A();
}
catch (int exception) {
std::cout << "예외 처리(main) : " << exception << std::endl;
}
return 0;
}
찾아보면서 애매했던 부분이 있다. 바로 "자료형이 일치하지 않아도 예외 데이터는 전달된다."
하지만, 실행 결과는 terminate()이다.
"예외 데이터는 전달된다." 이 전달된다는게 catch의 매개 타입으로써 암시적 형변환이던 어떤 일련의 과정을 거쳐서 catch에서 처리하는 타입으로 전해지는게 아니라 그냥 throw만 된다는 것이다.
throw는 했으나 일치하는 catch 절을 찾지 못해서 결국엔 3-4의 terminate().
int Absolute_Value(int n) {
if (n == 0)
throw 1.1;
else if (n > 0)
throw "음수를 입력하세요.";
return n * -1;
}
int main() {
int Number = 0;
std::cout << "음의 정수를 입력하거나 0을 입력해 종료해주세요. : ";
while (std::cin >> Number) {
try {
std::cout << "입력한 정수의 절댓값 : " << Absolute_Value(Number) << std::endl;
}
catch (const char* value) {
std::cout << Number << " 은 음의 정수가 아닙니다." << std::endl;
std::cout << "음의 정수를 입력하거나 0을 입력해 종료해주세요. : ";
}
catch (double value) {
std::cout << "종료합니다." << std::endl;
break;
}
}
return 0;
}
음의 정수를 입력받으면 양의 정수로 만드는 간단한 코드이다.
catch 블록을 여러개 생성해 다양한 타입의 예외처리를 수행할 수 있다.
+) 22.08.26 추가
문득 이런 생각이 들었다.
"nullptr로 예외 처리를 하는 경우가 많았는데 throw nullptr은 catch에서 어떻게 잡아낼까?"
int Absolute_Value(int n) {
if (n == 0)
throw nullptr;
else if (n > 0)
throw "음수를 입력하세요.";
return n * -1;
}
nullptr로 던졌으니까 catch에도?
#include <cstddef>
try {
do something ~
}
catch (std::nullptr_t) {
!!
}
결국은 terminate()였다. 분명히 이 곳에서는 된다고 했다.
하지만, 안되는걸 어떻게 하겠는가? 되는 방법을 찾아야지.
그래서 찾은 방법이 std::exception_ptr을 사용하는 것이다.
std::exception_ptr은 nullptr이고, 예외 객체를 가르키지 않는다고 했다.
A default-constructed std::exception_ptr
is a null pointer; it does not point to an exception object.
아래와 같이 사용했다.
int Absolute_Value(int n) {
if (n == 0)
{
std::exception_ptr p;
throw p;
}
else if (n > 0)
throw "음수를 입력하세요.";
return n * -1;
}
int main() {
int Number = 0;
std::cout << "음의 정수를 입력하거나 0을 입력해 종료해주세요. : ";
while (std::cin >> Number) {
try {
std::cout << "입력한 정수의 절댓값 : " << Absolute_Value(Number) << std::endl;
}
catch (const char* value) {
std::cout << Number << " 은 음의 정수가 아닙니다." << std::endl;
std::cout << "음의 정수를 입력하거나 0을 입력해 종료해주세요. : ";
}
catch (const std::exception_ptr&) {
std::cout << "종료합니다." << std::endl;
break;
}
}
return 0;
}
이렇게 하니까 정상적으로 nullptr 예외를 잡아낸다.
try ~ catch와 if ~ else는 서로 차이점이 분명하다.
try는 구문에서 예외가 발생하면 그 즉시 블록이 종료 되어 catch로 이동한다. if는 "구문"이다.
예외가 발생했을 때 try ~ catch 안의 모든 객체는 스코프를 벗어나 참조 할 수 없지만, if ~ else는 스코프가 벗어 나지 않게 되므로 try ~ catch 보다 더 위험하다.
하지만 try ~ catch 블록은 유지해야 할 정보도 많고 실제 예외가 발생했을 때 수행해야 할 동작이 많기 때문에 코드 크기나 예외 발생 시 처리 속도는 if ~ else가 더 빠르다고 한다.
try< ~ catch | if~else | |
---|---|---|
안전성 | 좋다. | 나쁘다. |
속도 | 느리다. | 빠르다. |
상황에 따라서 올바른 예외 처리 문법을 사용하면 되겠다.
공부를 하다 보니까 얼마나 다양하고 새로운 문법들이 많은지 새삼스럽게 느끼게 된다.
앞으로도 쭉 공부를 하겠지만, 새삼스럽게 공부의 중요성을 다시금 느낄 수 있었다.