Post

C++ - 예외처리

C++ 예외처리

1. 개요

C++의 예외 처리(Exception Handling)는 프로그램 실행 중 발생할 수 있는 오류(예외 상황)를 분리하고, 이를 포괄적이고 안전하게 다루기 위한 메커니즘이다. 전통적인 오류 코드는 반환값이나 전역 변수로 처리하지만, 예외 처리를 이용하면 오류 발생 지점을 호출 지점으로 강제 전파하고, 집중된 위치에서 일괄 처리할 수 있어 코드 가독성과 안전성이 향상된다.

2. 기본적인 처리 (try, catch, throw)

아래 예시는 0으로 나누었을 때 예외처리이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero");
    }
    return a / b;
}

int main() {
    try {
        std::cout << divide(10, 0) << "\n";
    }
    catch (const std::runtime_error& e) {
        std::cerr << "Runtime error: " << e.what() << "\n";
    }
    catch (const std::exception& e) {
        // std::runtime_error 외 다른 std 예외
        std::cerr << "Exception: " << e.what() << "\n";
    }
    catch (...) {
        // 그 외 모든 예외
        std::cerr << "Unknown exception caught\n";
    }
    return 0;
}
  • throw: 예외 상황이 발생한 지점에서 값을 던진다.
  • try: 예외 발생 가능 코드 블록을 감싼다.
  • catch: throw된 예외를 형식에 맞춰 잡아 처리한다.
  • catch(…): 그외 어떤 타입의 예외든 잡아낸다.

3. 표준 예외 계층 구조

c++ 에서 표준 라이브러리의 예외 계층 구조는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::exception
├─ std::bad_alloc
├─ std::bad_cast
├─ std::bad_typeid
├─ std::bad_exception
├─ std::logic_error
│   ├─ std::domain_error
│   ├─ std::invalid_argument
│   ├─ std::length_error
│   └─ std::out_of_range
└─ std::runtime_error
    ├─ std::overflow_error
    ├─ std::underflow_error
    └─ std::range_error
  • std::bad_alloc 발생 시점: new 연산자가 메모리 할당에 실패했을 때 헤더: <new>
    설명: 시스템 메모리가 부족하거나 할당 요청이 너무 클 때 던져지며, e.what()은 일반적으로 “std::bad_alloc”을 반환한다.

  • std::bad_cast
    발생 시점: dynamic_cast<Derived&>(base_ref)처럼 레퍼런스 기반 캐스트가 실패했을 때
    헤더: <typeinfo> 설명: 다운 캐스트나 횡단적 캐스트가 정확히 일치하지 않을 때 발생하며, 레퍼런스 캐스트만 예외를 던지고 포인터 캐스트(dynamic_cast<Derived*>(base_ptr))는 nullptr을 반환한다.

  • std::bad_typeid
    발생 시점: typeid(*ptr)에서 ptr이 nullptr일 때 헤더: <typeinfo>
    설명: 런타임에 타입 정보를 얻을 수 없을 때 발생하며, 마찬가지로 e.what()은 “std::bad_typeid”를 반환한다.

  • std::bad_exception
    발생 시점: 함수 선언부의 예외 사양(exception specification)에 명시되지 않은 예외를 던질 때
    헤더: <exception> 설명: C++17부터는 거의 사용되지 않는 기능이지만(예외 사양은 deprecated), 만약 void f() throw(int); 같이 선언된 함수가 int 이외 타입의 예외를 던지면 std::bad_exception으로 변환되어 전파된다.

  • std::logic_error
    발생 시점: 프로그래머의 논리적 실수나 계약(precondition) 위반이 감지될 때
    헤더: <stdexcept> 설명: 함수가 요구하는 조건(예: 인자의 유효 범위, 상태 머신의 올바른 상태 등)이 지켜지지 않았을 때 던져진다. throw std::logic_error(“Invalid argument”);처럼 메시지를 직접 지정할 수 있으며, e.what()은 해당 메시지를 반환한다.

  • std::runtime_error 발생 시점: 실행 중 환경 의존적 오류가 발생했을 때
    헤더: <stdexcept>
    설명: 파일 I/O 실패, 네트워크 오류, 메모리 부족 등 런타임 상황에서 복구할 수 없는 문제가 생기면 던져진다. 예를 들어 throw std::runtime_error(“File open failed”);처럼 사용하며, e.what()은 지정된 설명 문자열을 반환한다.

4. noexcept

해당 기능은 정의된 함수가 예외를 하나라도 던지지 않을 것이다라고 컴파일러에게 알려주기 위한 기능이다.
이게 왜 필요한가 싶지만 예외가 발생하지 않음을 컴파일러에게 알림으로써 좀 더 많은 최적화의 여지를 남기고, 예외 안정성을 좀 더 높일 수 있다.
특히, 소멸자에서 noexcept는 많이 사용된다.

1
2
3
void sayHello() noexcept { // 해당 함수에서 예외가 발생되지 않음을 알림
    std::cout << "Hello, world!\n";
}

만약, noexcept를 사용한 함수에서 예외 상황이 발생시, std::terminate()가 호출되어 비정상 종료되므로 noexcept 사용시 주의해야한다.

※ 예외 안정성

  • 기본 보장(Basic Guarantee)
    예외 발생 시 프로그램의 유효성은 유지되나, 일부 연산은 완료되지 않을 수 있음

  • 강력 보장(Strong Guarantee)
    예외 발생 시 ‘원자적’으로 실패 전 상태로 되돌아감(트랜잭션처럼)

  • 예외 없음 보장(No-throw Guarantee)
    예외가 절대 발생하지 않음을 보장(noexcept)

5. 사용자 정의 예외

사용자 정의 예외(Custom Exception)는 표준 라이브러리에서 제공하는 예외 클래스(std::exception 계열)를 상속받아, 프로그램 특유의 오류 상황을 더욱 구체적으로 표현하고 처리하기 위해 만드는 것이다.
방법은 총 두 가지이다.

1) std::exception 상속

what()만 오버라이드하면 간단히 구현 가능하다.
생성자에서 오류 메시지를 받아 멤버에 저장하고 what() 을 noexcept로 오버라이드해 메시지를 반환하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <exception>
#include <iostream>

class MyException : public std::exception {
public:
    MyException(const char* msg) : message(msg) {}
    const char* what() const noexcept override {
        return message;
    }
private:
    const char* message;
};

int main() {
  try {
      throw MyException("Something went wrong");
  }
  catch (const MyException& e) {
      std::cerr << "Caught MyException: " << e.what() << "\n";
  }
  return 0;
}

2) std::runtime_error 또는 std::logic_error 상속

이미 메시지 저장 기능을 갖고 있어 std::runtime_error(“msg”) 처럼 간단히 메시지를 전달 가능하다. 추가로 오류 관련 데이터(여기서는 file_name)를 멤버로 보관하여 출력할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdexcept>
#include <string>
#include <iostream>

class FileOpenError : public std::runtime_error {
public:
    FileOpenError(const std::string& filename)
      : std::runtime_error("Failed to open file: " + filename),
        file_name(filename) {}

    const std::string& filename() const noexcept {
        return file_name;
    }

private:
    std::string file_name;
};

int main() {
  try {
      throw FileOpenError("data.txt");
  }
  catch (const FileOpenError& e) {
      std::cerr << e.what() 
                << " (file: " << e.filename() << ")\n";
  }
  return 0;
}

3) 세부 에러 위치 반환

what() 외에도 아래처럼 에러 코드, 발생 위치 등을 멤버로 저장하여 반환가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <stdexcept>
#include <string>

// 사용자 정의 예외 클래스
class DetailedError : public std::exception {
public:
    DetailedError(int code, const std::string& msg, const char* file, int line)
        : code_(code), message_(msg), file_(file), line_(line) {}

    const char* what() const noexcept override {
        return message_.c_str();
    }
    int code() const noexcept { return code_; }
    const char* file() const noexcept { return file_; }
    int line() const noexcept { return line_; }

private:
    int code_;
    std::string message_;
    const char* file_;
    int line_;
};

// 예외 발생 매크로 정의
#define THROW_DETAILED(code, msg) \
    throw DetailedError((code), (msg), __FILE__, __LINE__)

// 예외를 발생시키는 함수
void performRiskyOperation(int value) {
    if (value < 0) {
        THROW_DETAILED(1001, "Negative value provided");
    }
    std::cout << "Value is valid: " << value << std::endl;
}

int main() {
    try {
        performRiskyOperation(-42);
    }
    catch (const DetailedError& e) {
        std::cerr << "Caught DetailedError:\n"
                  << "  Code    : " << e.code() << "\n"
                  << "  Message : " << e.what() << "\n"
                  << "  File    : " << e.file() << "\n"
                  << "  Line    : " << e.line() << std::endl;
        return e.code();
    }
    catch (const std::exception& e) {
        std::cerr << "Caught std::exception: " << e.what() << std::endl;
        return -1;
    }

    return 0;
}

위와 같이 해당 멤버 변수를 출력하는 함수를 사용하면 에러 위치까지 알려주는 사용자 정의 에러 클래스를 만들 수 있다.

참고자료

This post is licensed under CC BY 4.0 by the author.