예외란?
- 정상적인 상황에서 벗어난 모든 예외적인 상황들
// 인덱스 범위를 벗어난 배열으로의 접근
std::vector<int> v(3); // 크기가 3 인 벡터 만듦
std::cout << v.at(4); // ??
// 시스템에서 감당할 수 없는 과도한 메모리의 할당
std::vector<int> v(1000000000); // ??
기존 예외 처리 방식
- C 언어 차원에서 제공하는 예외 처리 방식 없음
- 결과 값을 확인하는 방식
- 이는 결과값을 일일이 확인해야하는 번거로움을 야기
bool func1(int *addr) {
if (func2(addr)) {
// Do something
}
return false;
}
bool func2(int *addr) {
if (func3(addr)) {
// Do something
}
return false;
}
bool func3(int *addr) {
// if array is not null, then you have an array with 1,000,000 ints.
char *c = (char *)malloc(1000000000);
// c가 정상적으로 할당됐으면 true
return c != NULL;
}
// 각 func1과 func2 에서 별도의 메모리 프로세스 과정을 거친다면,
// 함수별로 다시 결과 값이 정상적인지 확인하는 코드를 추가해야 함
int main() {
int *addr;
if (func1(addr)) {
// 잘 처리됨
} else {
// 오류 발생
}
}
throw
template <typename T>
class Vector {
// 사이즈를 할당하는 생성자
Vector (size_t size) : size_(size){
data_ = new T[size_];
for (int i =0 ;i < size_; ++i)
{
data_[i] = 3;
}
}
// 해당 인덱스의 값을 반환하는 함수
const T& at(size_t index) const {
if (index >= size_) {
// 예외를 발생시킨다!
throw out_of_range("vector 의 index 가 범위를 초과하였습니다.");
}
return data_[index];
}
// 소멸자
~Vector() { delete[] data_; }
}
- C++ 에는 예외를 던지고 싶다면, throw 로 예외로 전달하고 싶은 객체를 써주면 된다
- 기본적으로 예외로 아무 객체나 던져도 상관없다
- C++가 기본적으로 제공하는 여러 종류의 객체가 있으니 이를 활용하는 것이 좋다
- throw 위치에서 모든 함수가 종료된다
- 예외를 처리하는 부분으로 점프하여 실행한다
- stack에 쌓인 메모리는 모두 해제된다
try & catch
template <typename T>
class Vector {
// 위 클래스와 동일...
const T& at(size_t index) const {
if (index >= size_) {
throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
}
return data_[index];
}
// 위 클래스와 동일...
}
int main() {
Vector<int> vec(3);
// 기존 data 초기화
int index, data = 0;
std::cin >> index;
try {
data = vec.at(index);
} catch (std::out_of_range& e) {
std::cout << "예외 발생 ! " << e.what() << std::endl;
}
// 예외가 발생하지 않았다면 3이 출력되고,
// 예외가 발생하였다면 원래 data 에 들어가 있던 0이 출력된다.
std::cout << "읽은 데이터 : " << data << std::endl;
}
- try 안에서 예외가 발생할만한 코드가 실행
- 예외가 발생하지 않으면, try 안의 코드만 실행하고 catch의 부분은 무시됨
- 예외가 발생하면 stack의 모든 객체 소멸자들을 호출하고, 가장 가까운, 맞는 인자를 받는 catch 구문으로 이동
- 스택 풀기 (Stack Unwiding) 발생
- 현재 stack 정보를 정리하고 외부의 catch 구문을 찾기 위함
// 이 구문에서 예외 발생
// out_of_range를 throw
if (index >= size_) {
throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
}
// ...
// 이 구문으로 이동
// catch가 out_of_range를 받음
catch (std::out_of_range& e) {
std::cout << "예외 발생 ! " << e.what() << std::endl;
}
Stack Unwinding
- catch 로 점프 하면서 스택 상에서 정의된 객체들을 소멸시키는 과정
- 스택의 항상성을 유지하기 위함
- 스택 호출자와 피호출자가 푸쉬 횟수와 팝 횟수를 동일하게 유지하며 호출 전과 호출 이후의 상태를 유지
- 예외 발생 시 바로 외부의 catch 구문으로 이동한다면 스택의 항상성을 유지할 수 없음
- 항상성 유지를 위해 구문을 거슬러 올라가는 스택 풀기가 발생하는 것
#include <iostream>
#include <stdexcept>
class Resource {
public:
Resource(int id) : id_(id) {}
~Resource() { std::cout << "리소스 해제 : " << id_ << std::endl; }
private:
int id_;
};
int func3() {
Resource r(3);
throw std::runtime_error("Exception from 3!\n");
}
int func2() {
Resource r(2);
func3();
std::cout << "실행!" << std::endl;
return 0;
}
int func1() {
Resource r(1);
func2();
std::cout << "실행!" << std::endl;
return 0;
}
// 예외 발생 시 가장 가까운 catch 구문인
// main으로 이동
int main() {
try {
func1();
} catch (std::exception& e) {
std::cout << "Exception : " << e.what();
}
}
// 예외 발생 결과
// 리소스 해제 : 3
// 리소스 해제 : 2
// 리소스 해제 : 1
// Exception : Exception from 3!
// throw std::runtime_error("Exception from 3!\n")
// 없었다면 (예외 처리 없었다면)
// 리소스 해제 : 3
// 실행! (func2)
// 리소스 해제 : 2
// 실행! (func1)
// 리소스 해제 : 1
여러 종류의 예외
- catch는 여러 종류의 예외를 받아들일 수 있다
#include <iostream>
#include <string>
int func(int c) {
if (c == 1) {
throw 10;
} else if (c == 2) {
throw std::string("hi!");
} else if (c == 3) {
throw 'a';
} else if (c == 4) {
throw "hello!";
}
return 0;
}
int main() {
// 인풋에 따라 다른 예외 처리
int c;
std::cin >> c;
try {
func(c);
} catch (char x) {
std::cout << "Char : " << x << std::endl; // Char : a
} catch (int x) {
std::cout << "Int : " << x << std::endl; // Int : 10
} catch (std::string& s) {
std::cout << "String : " << s << std::endl; // String : hi!
} catch (const char* s) {
std::cout << "String Literal : " << s << std::endl; // String Literal : hello!
}
}
// 부모 클래스와 파생 클래스의 예외처리
#include <exception>
#include <iostream>
class Parent : public std::exception {
public:
virtual const char* what() const noexcept override { return "Parent!\n"; }
};
class Child : public Parent {
public:
// 부모 클래스의 what overriding
const char* what() const noexcept override { return "Child!\n"; }
};
int func(int c) {
if (c == 1) {
throw Parent();
} else if (c == 2) {
throw Child();
}
return 0;
}
int main() {
int c;
std::cin >> c;
try {
func(c);
} catch (Parent& p) {
std::cout << "Parent Catch!" << std::endl;
std::cout << p.what();
} catch (Child& c) {
std::cout << "Child Catch!" << std::endl;
std::cout << c.what();
}
}
// 결과
// Parent Catch!
// Parent!
// Parent Catch!
// Child!
- catch문이 여러 개일 경우, 가장 먼저 대입할 수 있는 객체를 받는다
- 첫 번째 구문인 catch (Parent& p)의 인자에 Child()를 대입할 수 있다
- 이 같은 경우를 방지하고자, 파생 클래스를 받는 catch 구문을 먼저 쓰는 것이 좋다
// ...
try {
func(c);
} catch (Child& c) { // 파생 클래스의 예외 처리를 먼저 받음
std::cout << "Child Catch!" << std::endl;
std::cout << c.what();
} catch (Parent& p) { // 부모 클래스의 예외 처리를 나중에 받음
std::cout << "Parent Catch!" << std::endl;
std::cout << p.what();
}
// ...
기타 예외를 처리하는 방법
// func에서는 runtime_error를 throw 하지만,
// 가장 가까운 catch 에서는 이를 받아주는 구문이 없다.
// 이 코드는 예외를 발생시키며 비정상적으로 종료된다.
#include <iostream>
#include <stdexcept>
int func() { throw std::runtime_error("error"); }
int main() {
try {
func();
} catch (int i) {
std::cout << "Catch int : " << i;
}
}
// 이를 지원하는 catch 구문 처리는 아래와 같다
try {
func(c);
} catch (int e) {
std::cout << "Catch int : " << e << std::endl;
} catch (...) { // try에서 발생한 모든 예외를 방을 수 있다
std::cout << "Default Catch!" << std::endl;
}
noexcept - 예외를 발생시키지 않는 함수
- 함수가 예외를 발생시키지 않는다는 것을 명시적으로 표현하는 키워드
- 하지만 이는 함수 안에서 throw 구문이 없음을 보장하지 않는다
- noexcept 처리된 함수가 예외를 발생시키면 프로그램은 비정상적으로 종료된다
- noexcept는 프로그래머가 컴파일러에게 주는 힌트에 지나지 않는다
- 함수가 throw 하지 않을 거라는 신뢰를 컴파일러에게 주는 것
- 컴파일러는 이로써 최적화를 진행할 수 있다
- C++11부터 소멸자들은 기본적으로 noexcept이다
void foo() { throw 1; }
void bar() noexcept { throw 1; }
int main() {
try {
// case 1
// 이 코드는 예외를 던지고 종료된다
foo();
// case 2
// 이 코드는 비정상적으로 종료된다
bar();
}
catch (int x) {
std::cout << "Error : " << x << std::endl;
}
return 0;
}
출처