핵심 정리
cpp
#include <stdexcept>
int divide(int a, int b) {
if (b == 0) throw std::invalid_argument("division by zero");
return a / b;
}
try {
int result = divide(10, 0);
} catch (const std::invalid_argument& e) {
std::cerr << e.what() << "\n"; // "division by zero"
} catch (const std::exception& e) {
std::cerr << "오류: " << e.what() << "\n";
}문법
스택 언와인딩 — 예외 전파 중 소멸자 자동 호출
throw가 실행되면 현재 스코프를 빠져나가며 catch를 찾을 때까지 스택을 거슬러 올라갑니다. 이 과정에서 지역 객체의 소멸자가 반드시 호출됩니다. RAII와 결합하면 예외 발생 시에도 자원이 안전하게 정리됩니다.
cpp
void process() {
auto file = std::make_unique<FileHandle>("data.txt"); // RAII 자원
auto lock = std::lock_guard<std::mutex>(mtx); // RAII 잠금
if (errorCondition) throw std::runtime_error("처리 실패");
// 예외가 발생해도 file과 lock의 소멸자가 자동 호출 — 자원 누수 없음
}표준 예외 계층 — 올바른 예외 타입 선택
표준 라이브러리는 std::exception을 루트로 하는 계층을 제공합니다. catch (const std::exception&)로 모든 표준 예외를 잡을 수 있습니다.
cpp
// 주요 표준 예외 타입
throw std::invalid_argument("잘못된 인자"); // 논리 오류 — 호출자 버그
throw std::out_of_range("인덱스 초과"); // 논리 오류
throw std::runtime_error("런타임 오류"); // 실행 중 예측 불가 오류
throw std::bad_alloc(); // 메모리 부족
// catch 순서 — 구체적인 것을 먼저
try {
riskyOperation();
} catch (const std::invalid_argument& e) { // 더 구체적 먼저
handleArgError(e);
} catch (const std::exception& e) { // 나머지 표준 예외
handleGeneric(e);
} catch (...) { // 모든 예외 (비표준 포함)
handleUnknown();
}사용자 정의 예외 — std::exception 상속
의미 있는 예외 타입이 필요하면 std::exception이나 그 하위 클래스를 상속합니다.
cpp
class NetworkError : public std::runtime_error {
int code_;
public:
NetworkError(int code, const std::string& msg)
: std::runtime_error(msg), code_(code) {}
int code() const { return code_; }
};
// 사용
try {
throw NetworkError(404, "리소스를 찾을 수 없음");
} catch (const NetworkError& e) {
std::cerr << "HTTP " << e.code() << ": " << e.what() << "\n";
} catch (const std::exception& e) {
std::cerr << e.what() << "\n";
}예외를 써야 할 때 vs 쓰지 말아야 할 때
예외는 예외적인 상황에만 씁니다. 정상 흐름 제어에 쓰면 코드가 이해하기 어려워지고 성능도 떨어집니다.
cpp
// ✅ 예외를 써야 할 때 — 복구 가능한 오류, 호출자에게 알려야 하는 오류
int readConfig(const std::string& path) {
std::ifstream f(path);
if (!f) throw std::runtime_error("설정 파일을 열 수 없음: " + path);
...
}
// ❌ 예외를 쓰지 말아야 할 때 — 정상 흐름 제어
bool contains(const std::vector<int>& v, int val) {
try {
// 예외를 제어 흐름에 사용 — 매우 나쁜 패턴
for (int x : v) if (x == val) throw true;
return false;
} catch (bool found) { return found; }
}
// ✅ 그냥 find 사용
bool contains2(const std::vector<int>& v, int val) {
return std::find(v.begin(), v.end(), val) != v.end();
}체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 호출자가 처리해야 할 오류 | throw std::runtime_error(msg) |
| 잘못된 인자 | throw std::invalid_argument(msg) |
| 모든 표준 예외 잡기 | catch (const std::exception& e) |
| 예외 + 자원 정리 | RAII (unique_ptr, lock_guard) |
| 예외를 전달하면 안 되는 함수 | noexcept 지정 |
주의할 점
예외가 발생할 수 있는 함수에서 raw 포인터로 자원을 관리하면 예외 발생 시 자원이 누수됩니다.
cpp
// ❌ raw 포인터 + 예외 — 누수 가능
void bad() {
int* buf = new int[100];
process(buf); // 예외 발생 시 delete[] 호출 안 됨
delete[] buf;
}
// ✅ RAII — 예외가 나도 소멸자에서 자동 해제
void good() {
auto buf = std::make_unique<int[]>(100);
process(buf.get()); // 예외가 나도 buf 소멸자가 delete[] 호출
}
// ❌ catch로 예외 잡아서 무시 — 오류를 숨김
try {
riskyOp();
} catch (...) {} // 모든 예외 무시 — 디버깅 불가능
// ✅ 최소한 로그는 남길 것
} catch (const std::exception& e) {
std::cerr << "예외 무시됨: " << e.what() << "\n";
}참고 링크
2 sources