본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::exception (98, 11)

by snowoods 2025. 10. 22.

Modern C++

 

C++ 예외(Exception) 처리

 

개요

C++ 예외 처리는 프로그램 실행 중에 발생하는 오류나 예외적인 상황(예: 0으로 나누기, 파일 열기 실패 등)을 처리하기 위한 강력하고 유연한 메커니즘입니다. 예외가 발생하면 일반적인 프로그램 흐름이 중단되고, 해당 예외를 처리할 수 있는 catch 블록으로 제어가 이동합니다. 이를 통해 오류 처리 코드를 기본 로직과 분리하여 코드의 가독성과 유지보수성을 높일 수 있습니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98: try, catch, throw 키워드와 std::exception 클래스가 표준으로 도입되었습니다. 기본적인 예외 처리의 틀이 완성되었습니다.
  • C++11: noexcept 지정자가 도입되었습니다. 함수가 예외를 발생시키지 않는다는 것을 명시적으로 선언하여 컴파일러가 최적화를 수행할 수 있도록 돕습니다.

 

내용 설명

C++ 예외 처리는 세 가지 주요 키워드로 구성됩니다.

  • throw: 예외가 발생했음을 알리는 데 사용됩니다. throw 표현식은 예외 객체를 생성하고 던집니다. 던져진 객체는 어떤 타입이든 될 수 있지만, 일반적으로 std::exception을 상속하는 클래스의 객체를 사용하는 것이 좋습니다.
  • try: 예외가 발생할 가능성이 있는 코드를 감싸는 블록입니다. try 블록 내에서 예외가 발생하면, 프로그램은 즉시 실행을 멈추고 해당 예외를 처리할 수 있는 catch 블록을 찾습니다.
  • catch: try 블록에서 발생한 예외를 처리하는 코드 블록입니다. catch는 특정 타입의 예외를 받아서 처리할 수 있으며, 여러 개의 catch 블록을 사용하여 다양한 타입의 예외를 처리할 수 있습니다. 예외를 잡을 때는 값보다는 const 참조(const T&)로 받는 것이 효율적입니다.
  • noexcept (C++11): 함수가 예외를 던지지 않음을 명시합니다. 만약 noexcept로 선언된 함수에서 예외가 발생하면, 예외 처리 메커니즘을 통하지 않고 즉시 std::terminate가 호출되어 프로그램이 종료됩니다. 이는 컴파일러에게 최적화 기회를 제공합니다.

 

예제 코드

사용자로부터 입력받은 값으로 나눗셈을 수행하며, 0으로 나눌 경우 std::runtime_error 예외를 던지는 예제입니다.

#include <iostream>
#include <stdexcept> // std::runtime_error를 사용하기 위해 필요

// noexcept(false)는 이 함수가 예외를 던질 수 있음을 명시합니다. (기본값이므로 생략 가능)
double div(double x, double y) noexcept(false)
{
    if (y == 0.0)
    {
        // C-스타일 문자열 대신 std::exception의 파생 클래스를 사용하는 것이 좋습니다.
        throw std::runtime_error("Division by zero!");
    }

    return x / y;
}

int main()
{
    double x = 10.0;
    double y;

    std::cout << "We will compute (x/y)" << '\n';
    std::cout << "Please enter a value for y= ";
    std::cin >> y;
    std::cout << '\n';

    try
    {
        double z = div(x, y);
        std::cout << "x/y = " << z << '\n';
    }
    // 예외를 const 참조로 받는 것이 일반적입니다.
    catch (const std::runtime_error& e)
    {
        // e.what() 멤버 함수로 오류 메시지를 얻을 수 있습니다.
        std::cerr << "Error: " << e.what() << '\n';
    }

    return 0;
}

 

실행 결과

정상 실행 (y = 5 입력 시)

We will compute (x/y)
Please enter a value for y= 5

x/y = 2

 

예외 발생 (y = 0 입력 시)

We will compute (x/y)
Please enter a value for y= 0

Error: Division by zero!

 

활용 팁

  • 예외는 값으로 던지고, 참조로 받으세요 (throw by value, catch by reference): 예외 객체를 const 참조로 받으면 불필요한 복사를 피하고, 다형성(Polymorphism)을 활용하여 부모 클래스 타입으로 자식 클래스 예외를 받을 수 있습니다.
  • RAII(Resource Acquisition Is Initialization)를 활용하세요: 예외가 발생하면 스택이 풀리면서 지역 변수들의 소멸자가 호출됩니다. 스마트 포인터(std::unique_ptr, std::shared_ptr)나 std::lock_guard와 같은 RAII 객체를 사용하면 예외 발생 시에도 자원이 안전하게 해제되도록 보장할 수 있습니다.
  • 소멸자에서는 예외를 던지지 마세요: 소멸자에서 예외를 던지는 것은 매우 위험합니다. 스택 풀기(stack unwinding) 과정에서 다른 예외를 처리하던 중 소멸자가 예외를 던지면 std::terminate가 호출되어 프로그램이 즉시 종료됩니다. 소멸자는 noexcept(true)로 간주하는 것이 좋습니다.
  • 예외는 정말 '예외적인' 상황에만 사용하세요: 예외 처리는 비용이 따르므로, 일반적인 프로그램 제어 흐름(if-else 등)으로 처리할 수 있는 로직에 남용해서는 안 됩니다.