
C++ 예외 처리 : try-catch, throw, noexcept
예외 처리는 C++ 프로그래밍에서 오류를 감지하고 대응하는 매커니즘.
적절히 활용한다면 안정적인 프로그램을 작성할 수 있으며,
오류 처리 로직을 메인코드와 분리해서 가독성을 높일 수 있다.
1. Try-Catch 문의 기본 개념
기본 구조와 동작 방식
C++의 예외처리는 try, throw, catch 세가지 키워드를 중심으로 이뤄진다.
#include <iostream>
using namespace std;
double SafeDivide(double num, double den)
{
if (den == 0) {
throw invalid_argument("Divide by zero");
}
return num / den;
}
int main()
{
try {
cout << SafeDivide(5, 2) << endl;
cout << SafeDivide(10, 0) << endl; // 0으로 나누면 예외 발생 -> Catch문으로 이동
cout << SafeDivide(3, 3) << endl; // 실행되지 않음
}
catch (const std::exception& e) {
cout << "error exception : " << e.what() << endl;
}
return 0;
}
Try-Catch 문의 구성 요소
1. try 블록
- 예외가 발생할 수 있는 코드를 포함하는 블록.
- try 안에서 예외(throw)가 발생하면, 그 즉시 try 블록을 빠져나가고, catch 블록으로 넘어감.
2. throw 키워드
- 예외를 발생시키는 키워드.
- 특정 조건에서 오류라고 판단된다면, 예외 객체를 던져(throw) catch로 전달.
3. catch 블록
- throw 된 예외를 받아서 처리.
- catch(const std::exception& e)는 C++ 표준 라이브러리의 예외 타입을 받아
메시지를 출력하거나 다른 처리를 함.
실행 흐름 분석
- SafeDivide(5, 2) → 정상적으로 2.5 출력
- SafeDivide(10, 0) → 분모가 0 → 예외 발생 → "Divide by zero" 메시지와 함께 invalid_argument 예외 발생
- catch 블록으로 넘어감 → 예외 메시지 출력
- SafeDivide(3, 3)는 실행되지 않음 (예외 발생 후 try 블록 탈출)
출력 결과:
2.5
error exception : Divide by zero
주의사항: try-catch 범위
예외 처리는 try 블록 내에서만 작동한다..
try 블록 외부에서 발생한 예외는 처리되지 않고 프로그램이 중단됨.
int main()
{
try {
// try 블록이 비어 있음
}
catch (const std::exception& e) {
cout << "error exception : " << e.what() << endl;
}
cout << SafeDivide(5, 2) << endl; // 2.5 출력
cout << SafeDivide(10, 0) << endl; // 예외 발생
cout << SafeDivide(3, 3) << endl; // 실행되지 않음
return 0;
}
- SafeDivide(5, 2) → 정상 출력 (2.5)
- SafeDivide(10, 0) → throw 발생 (예외 던져짐)
- 하지만 try-catch 범위 밖이기 때문에 예외를 처리하지 못함
- 프로그램은 즉시 종료되고 런타임 오류 메시지가 출력된다.
- catch 블록은 실행되지 않음
- SafeDivide(3, 3) → 도달하지 않음
2. throw 키워드와 예외 발생
throw 키워드는 C++에서 예외를 발생시키는 데 사용된다.
기본 사용법
throw 예외객체;
예외가 발생하면 현재 실행 중인 함수는 즉시 종료되고,
가장 가까운 try 블록으로 제어가 이동해서 해당 예외를 처리할 수 있는 catch 블록이 실행.
예제 코드
#include <iostream>
#include <stdexcept> // 표준 예외 클래스 사용
using namespace std;
void CheckAge(int age) {
if (age < 0) {
throw invalid_argument("나이는 음수가 될 수 없습니다.");
}
cout << "나이: " << age << endl;
}
int main() {
try {
CheckAge(-5); // 예외 발생!
}
catch (const exception& e) {
cout << "예외 발생: " << e.what() << endl;
}
return 0;
}
출력 결과:
예외 발생: 나이는 음수가 될 수 없습니다.
throw로 던질 수 있는 것들
| 기본 타입 | 숫자, 문자 등 | throw 42; |
| 문자열 | C 스타일 문자열 | throw "에러 발생"; |
| 객체 | 표준/사용자 정의 예외 객체 | throw std::runtime_error("메시지"); |
| 사용자 정의 예외 | 직접 정의한 예외 클래스 | throw MyCustomException(); |
빈 throw (Re-throwing)
catch 블록 내에서 throw;만 사용하면 현재 처리 중인 예외를 다시 던질 수 있다.
try {
// 코드...
}
catch (const std::exception& e) {
// 일부 처리 수행
cout << "예외 처리 중..." << endl;
// 다시 예외 던지기
throw; // 현재 예외를 다시 던짐
}
3. 표준 예외 클래스
C++ 표준 라이브러리는 <stdexcept> 헤더에 다양한 예외 클래스를 제공한다.
주요 표준 예외 클래스
| std::exception | 모든 표준 예외의 기본 클래스 | 일반적인 예외 처리 |
| std::invalid_argument | 잘못된 인자가 전달됨 | 함수 매개변수 검증 |
| std::out_of_range | 범위를 벗어난 접근 | 배열, 벡터 인덱싱 |
| std::runtime_error | 실행 시간에 발생하는 오류 | 예상치 못한 런타임 오류 |
| std::logic_error | 논리적 오류 | 프로그램 논리상 오류 |
| std::length_error | 너무 큰 크기가 요청됨 | 메모리 할당 제한 |
| std::domain_error | 수학적 도메인 오류 | 허용되지 않는 연산 |
사용자 정의 예외 클래스
필요에 따라 자신만의 예외 클래스를 정의할 수 있다.
#include <iostream>
#include <exception>
using namespace std;
// 사용자 정의 예외 클래스
class NetworkError : public std::exception {
private:
string message;
public:
NetworkError(const string& msg) : message(msg) {}
// what() 메서드 오버라이드
const char* what() const noexcept override {
return message.c_str();
}
};
int main() {
try {
throw NetworkError("서버 연결 실패");
}
catch (const NetworkError& e) {
cout << "네트워크 오류: " << e.what() << endl;
}
catch (const exception& e) {
cout << "일반 오류: " << e.what() << endl;
}
return 0;
}
출력 결과:
네트워크 오류: 서버 연결 실패
4. 처리되지 않은 예외와 terminate()
C++에서 예외가 발생했는데 catch 블록에서 처리되지 않으면, terminate() 함수가 호출되고 프로그램이 종료된다.
set_terminate()로 종료 핸들러 지정하기
set_terminate() 함수를 사용해서 처리되지 않은 예외 발생시에 호출될 핸들러 함수를 지정할 수 있다.
#include <iostream>
#include <exception>
using namespace std;
// 사용자 정의 terminate 핸들러
void MyTerminate() {
cout << "Unhandled Exception\\n" << flush;
_Exit(1); // std::exit이 아니라 _Exit는 cleanup 없이 바로 종료
}
int main() {
// terminate 핸들러 등록
set_terminate(MyTerminate);
// 처리되지 않은 예외 발생
throw 1;
return 0;
}
출력 결과:
Unhandled Exception
try-catch가 있는 경우와 없는 경우 비교
#include <iostream>
#include <exception>
#include <cstdlib>
using namespace std;
// terminate 핸들러 정의
void MyTerminate() {
cout << "[terminate 호출됨]" << endl;
exit(1);
}
// 예외를 던지는 함수
void CauseError() {
throw runtime_error("예외 발생!");
}
int main() {
// terminate 핸들러 등록
set_terminate(MyTerminate);
try {
cout << " 예외 발생 직전..." << endl;
CauseError(); // 예외 발생
cout << "이 줄은 예외 발생 시 실행되지 않음!" << endl;
}
catch (const exception& e) {
// 예외를 처리했기 때문에 terminate는 호출되지 않음
cout << "[예외 처리됨] 메시지: " << e.what() << endl;
}
cout << "프로그램 정상 종료" << endl;
return 0;
}
출력 결과:
예외 발생 직전...
[예외 처리됨] 메시지: 예외 발생!
프로그램 정상 종료
만약 try-catch가 없었을 시
int main() {
set_terminate(MyTerminate);
CauseError(); // 예외가 처리되지 않음 -> terminate 호출
cout << "이 줄은 실행되지 않음!" << endl;
return 0;
}
출력 결과:
[terminate 호출됨]
종료 방식 차이: exit() vs _Exit()
| std::exit(code) | 정상적인 종료 루틴을 통해 종료됨 (전역 객체 소멸자 호출 등 포함) |
| std::_Exit(code) | cleanup 없이 즉시 종료 (더 빠르지만 리소스 해제 없음) |
5. noexcept 키워드
C++11부터 도입된 noexcept 키워드는 함수가 예외를 던지지 않음을 명시적으로 선언한다.
기본 문법
void func() noexcept; // 이 함수는 예외를 던지지 않음
noexcept로 선언된 함수 내에서 예외가 발생하면, catch 블록에서 처리할 수 없고 즉시 terminate()가 호출됨.
예제: noexcept 함수에서 예외 발생
#include <iostream>
#include <stdexcept>
using namespace std;
void MyTerminate() {
cout << "[terminate 호출됨 - noexcept 위반]" << endl;
exit(1);
}
void NoExceptFunc() noexcept {
throw runtime_error("예외 발생!"); // 예외를 던지면 terminate 호출됨
}
int main() {
set_terminate(MyTerminate);
try {
NoExceptFunc(); // 예외 발생 -> catch 불가능 -> terminate 실행
}
catch (const exception& e) {
// 이 블록은 실행되지 않음
cout << "예외 처리: " << e.what() << endl;
}
cout << "이 줄은 절대 실행되지 않음!" << endl;
return 0;
}
출력 결과:
[terminate 호출됨 - noexcept 위반]
조건부 noexcept
C++11에서는 조건부로 noexcept를 적용할 수 있다.
void MightThrow();
// MightThrow()가 noexcept인 경우에만 SafeFunction()도 noexcept
void SafeFunction() noexcept(noexcept(MightThrow())) {
MightThrow();
}
noexcept vs throw()
C++98에서는 throw() 지정자를 사용했지만, C++11 이후로는 noexcept를 사용하는 것이 권장.
| noexcept | C++11 이상, 최적화, 명시적 예외 금지 |
| throw() | C++98 스타일, 의미는 비슷하지만 deprecated 됨 |
6. 이동 의미론과 예외 처리의 관계
C++11에서 도입된 이동 의미론(Move Semantics)은 예외 처리와 밀접한 관련이 있다.
STL과 noexcept의 관계
STL 컨테이너들(특히 std::vector)은 요소를 이동할 때 이동 생성자가 noexcept인지 확인한다.
noexcept로 선언된 이동 생성자는 예외 안전성이 보장되어 더 효율적인 이동 연산이 가능하다.
#include <iostream>
#include <vector>
using namespace std;
class Safe {
public:
Safe() {}
Safe(const Safe&) { cout << "Copy\\n"; }
Safe(Safe&&) noexcept { cout << "Move (noexcept)\\n"; }
};
class Risky {
public:
Risky() {}
Risky(const Risky&) { cout << "Copy\\n"; }
Risky(Risky&&) { cout << "Move (but throws!)\\n"; } // noexcept 아님
};
int main() {
cout << "--- vector<Safe> ---\\n";
vector<Safe> vs;
vs.reserve(2);
vs.push_back(Safe());
vs.push_back(Safe()); // Move 사용됨 (noexcept)
cout << "\\n--- vector<Risky> ---\\n";
vector<Risky> vr;
vr.reserve(2);
vr.push_back(Risky());
vr.push_back(Risky()); // Move 안 쓰고 Copy fallback
}
출력 결과:
--- vector<Safe> ---
Move (noexcept)
Move (noexcept)
--- vector<Risky> ---
Move (but throws!)
Copy
일단 Safe의 이동 생성자가 noexcept로 선언되어 있다.
그렇다면 vector가 안전하게 이동 생성자를 생성하여 사용이 가능하다.
즉, 임시객체가 생성되며 vector 내부로 이동하여 vector가 안전하게 기능 수행이 가능하지만,
Risky클래스의 이동생성자 같은경우 noexcept가 없다.
그렇기에 vector가 예외 안전성을 위해서 복사 생성자를 대신 사용하지 못한다.
그래서 Risky의 경우 Move를 사용하지 않고서 바로 copy fallback이 발생하는 것이다.
때문에 Risky에서 vector가 구현된 이동 생성자 보다 더 안전한 복사 생성자를 선택하는 것.
해당 원칙은 예외를 던지지 않는다고 보장할 수 있을 때만 이동 생성자를 사용하고,
그렇지 않으면 복사를 사용한다는 C++의 내부 구조를 보여주는 것.
std::move_if_noexcept(위에 부분 재정리)
std::move_if_noexcept 함수는 객체가 noexcept 이동 생성자를 가지고 있을 때만 이동을 시도하고,
그렇지 않으면 복사를 사용한다.
#include <iostream>
#include <utility>
using namespace std;
class A {
public:
A() {}
A(const A&) { cout << "Copy\\n"; }
A(A&&) noexcept { cout << "Move\\n"; }
};
class B {
public:
B() {}
B(const B&) { cout << "Copy\\n"; }
B(B&&) { cout << "Move but throws!\\n"; } // noexcept 아님
};
int main() {
A a;
B b;
A a2 = std::move_if_noexcept(a); // move 사용
B b2 = std::move_if_noexcept(b); // copy 사용 (move는 noexcept 아님)
}
출력 결과:
Move
Copy
이동 생성자와 noexcept
이동 생성자(move constructor)는 다른 객체의 자원을 "훔쳐" 자신의 것으로 만드는 생성자이다.
class MyString {
public:
// 이동 생성자
MyString(MyString&& other) noexcept
: data(other.data), length(other.length) {
// 원본 객체의 리소스를 무효화
other.data = nullptr;
other.length = 0;
}
private:
char* data;
size_t length;
};
이동 생성자가 noexcept로 선언되면
- STL 컨테이너가 안전하게 이동 연산을 사용할 수 있음
- 예외가 발생해도 프로그램이 안전하게 종료됨
- 컴파일러가 추가 최적화를 수행할 수 있음
7. 실제 개발에서의 예외 처리 전략
예외 처리의 장단점
장점:
- 오류 처리 코드를 메인 로직과 분리하여 가독성 향상
- 오류 상황을 놓치기 어려움 (처리하지 않으면 프로그램 종료)
- 계층적인 오류 처리 가능
단점:
- 성능 오버헤드 발생 가능
- 예외를 완벽하게 처리하기 어려움
- 모든 코드 경로에 대한 예외 처리가 필요하면 복잡해짐
대안적 접근법: 오류 코드 반환
일부 프로젝트나 회사에서는 예외 대신 오류 코드를 사용하기도 한다.
enum class ErrorCode {
Success = 0,
InvalidArgument = 1000,
FileNotFound = 1001,
NetworkError = 2000,
// ...
};
ErrorCode ProcessFile(const string& filename) {
if (!FileExists(filename)) {
return ErrorCode::FileNotFound;
}
// 처리 로직...
return ErrorCode::Success;
}
int main() {
ErrorCode result = ProcessFile("data.txt");
if (result != ErrorCode::Success) {
cout << "오류 발생: " << static_cast<int>(result) << endl;
// 오류 처리...
}
return 0;
}
위에 오류코드 반환 기능 개발시 권장 사항
- 일관성 유지: 프로젝트 전체에서 예외 처리 방식을 일관되게 유지
- 적절한 범위 설정: 너무 넓은 try 블록은 피하고, 예외가 발생할 수 있는 코드만 포함
- 구체적인 예외 처리: 가능하면 구체적인 예외 유형부터 처리
- 리소스 관리에 RAII 사용: 생성자/소멸자를 통한 자원 관리로 예외 발생 시에도 리소스 누수 방지
- noexcept 적절히 사용: 정말 예외가 발생하지 않는 함수에만 noexcept 사용
- 문서화: 함수가 어떤 예외를 던질 수 있는지 문서화
'Game DevTip > C++' 카테고리의 다른 글
| 17. C++ 람다(Lamda) 표현식 (3) | 2025.07.27 |
|---|---|
| 16. C++ 순수 가상함수(추상 클래스 & 인터페이스)에 대해서 (5) | 2025.07.26 |
| 14. C++ 이동의미론과 rvalue & lvalue (0) | 2025.07.22 |
| 13. C++ 스마트 포인터에 대해서 (1) | 2025.07.19 |
| 12. C++ 배열에 대해서 (1) | 2025.07.16 |
댓글