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;
}
위와 같이 해당 멤버 변수를 출력하는 함수를 사용하면 에러 위치까지 알려주는 사용자 정의 에러 클래스를 만들 수 있다.