본문 바로가기
프로그래밍/C++

C++ ] 예외처리 try-catch와 throw

by eteo 2023. 5. 28.

 

 

 

C++에서 try-catch 문은 예외가 발생할 수 있는 코드 블록을 감싸고, 발생한 예외를 적절하게 처리하는 메커니즘을 제공한다.

 

try-catch 기본 구문

 

try {
    // 예외가 발생할 수 있는 코드
} catch (예외_유형1& e) {
    // 예외_유형1에 해당하는 예외 처리
    throw; // 다시 던질 수도 있음
} catch (예외_유형2& e) {
    // 예외_유형2에 해당하는 예외 처리
    // 다시 던지지 않고 처리하고 끝남
} catch (...) {
    // 기타 모든 예외를 처리하는 블록
}

 

  • try 블록: 예외가 발생할 수 있는 코드를 포함하는 블록. 이 블록 내에서 예외가 발생하면 예외를 던진다.
  • catch 블록: 발생한 예외를 처리하기 위한 블록. catch 블록은 발생한 예외의 유형에 따라 여러 개를 사용할 수 있다. 가장 specific한 예외 유형부터 순서대로 체크하며, 마지막 catch 블록은 모든 예외를 처리하기 위해 사용된다. 이 블록은 예외의 유형을 명시하지 않고, ...으로 표시할 수 있다.
  • 예외 유형: 처리할 예외의 유형을 나타내는 C++ 예외 클래스. 예외 유형은 사용자 정의 예외 클래스일 수도 있고, 표준 예외 클래스 중 하나일 수도 있다.
  • e: 예외 객체의 이름. 예외 객체에는 발생한 예외에 대한 정보가 포함되어 있으며, catch 블록 내에서 참조할 수 있다.

 

 

 

 

 

사용자가 직접 std::runtime_error 예외를 던지고(throw) 해당 예외 객체에 문자열 메시지를 전달하는 예제

 

#include <iostream>

int main() {
    try {
        int dividend, divisor;
        
        std::cout << "Enter dividend: ";
        std::cin >> dividend;
        
        std::cout << "Enter divisor: ";
        std::cin >> divisor;
        
        if (divisor == 0) {
        // std::runtime_error 예외 던지면서 메시지 전달
            throw std::runtime_error("Division by zero is not allowed.");
        }
        
        double result = static_cast<double>(dividend) / divisor;
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
    // 예외 객체를 통해 메시지 출력
        std::cout << "An exception occurred: " << e.what() << std::endl;
    }
    
    return 0;
}

 

 

위의 예시 코드에서 0으로 나누려고 시도하면 아래 에러 문구가 출력된다. 예외를 throw 를 통해서 던지면, catch 블록에서 이를 잡아서 처리하는 것이다

 

An exception occurred: Division by zero is not allowed.

 

 

throw할 수 있는 예외는 다음과 같다.

 

✔ C++ 표준 라이브러리의 std::exception 클래스를 상속받는 여러 예외 클래스

 

  • std::runtime_error : 프로그램 실행 중 발생하는 오류
  • std::logic_error : 논리 오류. 예를 들어, 잘못된 인자를 전달한 경우 등
  • std::invalid_argument : 잘못된 인수를 전달한 경우 발생하는 예외 클래스
  • std::out_of_range : 범위를 벗어난 인덱스나 값에 접근한 경우 발생하는 예외 클래스
  • std::bad_alloc : 동적 메모리 할당에 실패한 경우 발생하는 예외 클래스

이 외에도 다양한 예외 클래스가 있다.

사용자 정의 예외 객체를 만들어 예외를 throw할 수도 있고, 명시적으로 throw 문을 사용하지 않아도 일부 상황에서 자동으로 예외를 발생시키고 처리할 수 있는 예외 처리 메커니즘을 제공된다.

 

 

 

 

✔ std::exception은 C++ 표준 예외 클래스의 기본 클래스로 이 매개변수 유형을 사용하면 모든 종류의 예외를 잡을 수 있다. 하지만, catch 블록에서 std::exception을 사용하여 모든 예외를 처리하는 것은 예외 처리의 마지막 수단으로 사용되어야 한다.

왜냐하면 예외의 구체적인 유형을 알 수 없기 때문에, 다운캐스팅(dynamic_cast)의 방법을 쓰지 않는 이상 예외의 세부 정보를 알 수 없고 적절한 조치를 취하는 것이 어렵기 때문이다. 때문에 최대한 예외 유형에 따라 명시적인 catch 블록을 먼저 사용하여 처리하는 것이 좋다.

 

 

예를들어 보자.

 

std::filesystem::filesystem_error는 <filesystem> 헤더에서 정의된 특정 예외 클래스로 파일 시스템 작업의 실패 원인에 대한 정보를 보다 자세히 제공한다. 이렇게 보다 구체적인 예외 클래스를 사용하면 예외를 더 쉽게 처리하고, 예외 객체에서 세부 정보에 접근할 수 있다.

 

#include <iostream>
#include <filesystem>

int main() {
    std::filesystem::path sourceFile("path/to/source.txt");
    std::filesystem::path destinationFile("path/to/destination.txt");
    
    try {
        std::filesystem::rename(sourceFile, destinationFile);
        std::cout << "File moved successfully." << std::endl;
    } catch (const std::filesystem::filesystem_error& e) {
        std::cout << "Failed to move file: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cout << "exception : " << e.what() << std::endl;
    }
    
    return 0;
}

 

 

 

 

 

🔍 throw는 언제 해야할까?

 

예외가 발생(throw)하면, 마치 return;처럼 현재 함수의 나머지 코드는 실행되지 않고, 곧바로 호출자를 향해 전파된다. 예외를 catch하지 않는 한 계속해서 상위로 전파되며, 최종적으로 어디서도 처리되지 않으면 std::terminate()로 인해 프로그램이 종료된다.

따라서 throw는 단순 오류나 예측 가능한 상황보다는, 정상적인 흐름으로 처리할 수 없는 예외 상황에서 사용하는 것이 좋다.

 

 

 

 

🔍 catch는 언제 해야할까?

 

함수가 서로를 호출하는 계층 구조에서는 예외 처리의 책임을 어디서 가질지 정하는 것이 중요하다.

아래 예시코드는 callee()에서 예외가 발생했을 때 caller()를 거쳐 main()으로 전파되고, 최종적으로 main()에서 예외를 잡아 처리하는 구조이다.

 

void callee() {
    throw std::runtime_error("something went wrong in callee()");
}

void caller() {
    callee(); // 예외가 여기서 발생하지만, 직접 처리하지 않음
}

int main() {
    try {
        caller();
    } catch (const std::exception& e) {
    	// 최상위 호출자에서 예외를 catch
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
}

 

모든 함수에서 try-catch를 쓰는 것은 필요 이상으로 코드를 복잡하게 만들 수 있기 때문에 예외는 꼭 필요한 위치에서만 처리하는 것이 좋다. 복구 가능한 경우에는 그 자리에서 처리하고, 그렇지 않으면 예외를 상위로 넘겨서 최종적으로 사용자에게 메시지를 보여주는 식으로 정리하는 것이 일반적이다.