KoreanFoodie's Study

C++ 기초 개념 11 : 예외처리 본문

Tutorials/C++ : Beginner

C++ 기초 개념 11 : 예외처리

GoldGiver 2022. 4. 19. 10:18

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

throw 로 예외 발생시키기

예외를 발생시키는 예시 코드를 보자.

// 생략 ...
const T& at(size_t index) const {
  if (index >= size) {
    // 예외를 발생시킨다!
    throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
  }
  return data[index];
}

위에서는 범위 초과를 알리는 메시지를 출력한다. C++ 는 예외를 던지고 싶다면 throw 로 예외로 전달하고 싶은 객체를 써주면 된다. C++ 표준에는 overflow_error, length_error, runtime_error 등의 다양한 예외 객체가 정의되어 있다.

이렇게 예외를 throw 하게 되면, throw 아래 있는 명령문은 실행되지 않는다. throw 에서 예외를 처리하는 부분으로 바로 점프하기 때문이다! 이 과정에서, stack 에 생성되었던 객체들을 빠짐없이 소멸시켜 준다! (stack unwinding)

 

 

try - catch 구문

이번에도 예시 코드를 통해 동작을 파악하자.

  try {
    data = vec.at(index);
  } catch (std::out_of_range& e) {
    std::cout << "예외 발생 ! " << e.what() << std::endl;
  }

try 안에서 예외가 발생하면, catch 구문으로 이동하고, 예외가 발생하지 않았다면 catch 구문은 무시된다. 예외가 발생하면 stack 에 생성된 모든 객체들의 소멸자들이 호출되고, 가장 가까운 catch 문으로 점프한다.

what( ) 함수는 예외에 관한 내용을 저장하는 문자열 필드를 들여다 보는 함수이다. 이 경우 이미 전달한 문장인 "vector의 index가 범위를 초과하였습니다" 가 나오게 된다.

 

 

스택 풀기 (stack unwinding)

try 구문에서 예외가 발생했을 때, 가장 가까운 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;
}

int main() {
  try {
    func1();
  } catch (std::exception& e) {
    std::cout << "Exception : " << e.what();
  }
}

위의 결과값은 다음과 같다.

리소스 해제 : 3
리소스 해제 : 2
리소스 해제 : 1
Exception : Exception from 3!

 

func1, func2 는 예외를 받는 구문이 없으므로, main 함수의 catch 가 해당 예외를 출력해준다. 중요한 것은, catch 로 점프하면서 스택 상에서 정의된 객체들을 소멸해주게 되는데, 이를 스택 풀기 (stack unwinding) 이라고 부른다!

 

 

여러 종류의 예외 받기

#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;
  } catch (int x) {
    std::cout << "Int : " << x << std::endl;
  } catch (std::string& s) {
    std::cout << "String : " << s << std::endl;
  } catch (const char* s) {
    std::cout << "String Literal : " << s << std::endl;
  }
}

위의 경우처럼, catch 는 여러 종류의 throw 된 객체를 모두 받을 수 있다.

 

그렇다면 파생 클래스에서 catch 가 실행된다면 어떻게 될까?

#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:
  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();
  }
}

1을 넣으면 "Parent Catch" 가 출력되지만, 2 를 넣으면 "Child Catch" 가 아닌 "Parent Catch" 가 출력된다. 왤까?

catch 문의 경우 가장 먼저 대입될 수 있는 객체를 받는데,

Parent& p = Child();

는 가능하기 때문에 Parent catch 가 이를 먼저 받아버린다. 따라서 파생 클래스의 예외 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();
  }

참고로, 일반적으로 예외객체는 std::exception 을 상속받는 것이 좋다. 왜냐하면 표준 라이브러리의 유용한 함수들 (nested_exception 등) 을 사용할 수 있기 때문이다!

 

 

모든 예외 받기

아래 코드처럼, 어떤 예외를 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;
  }
}

컴파일 후 실행하면 runtime_error 코드가 발생하며 프로그램이 종료한다. (여기서는 std::runtime_error 객체를 catch 의 인자로 써 주어야 함)

 

만약 타입에 상관없이 '나머지 전부'의 예외를 받고 싶으면, 다음과 같은 구문을 쓰면 된다!

  try {
    func(c);
  } catch (int e) {
    std::cout << "Catch int : " << e << std::endl;
  } catch (...) {
    std::cout << "Default Catch!" << std::endl;
  }

 

 

 

예외를 발생시키지 않는 함수

int foo() noexcept {}

int bar() noexcept { throw 1; }

위처럼, noexcept 를 붙이게 되면, 예외를 발생시키지 않는 함수로 정의할 수 있다. 다만 noexcept 가 붙은 함수에서 예외를 발생시키면 예외 발생시 예외가 제대로 처리되지 않고 프로그램이 종료한다! (bar 함수처럼)

C++ 11 부터, 소멸자들은 기본적으로 noexcept 이다. 절대로 소멸자에서 예외를 던지면 안된다! 프로그램의 종료 혹은 미정의 동작을 초래할 것이기 때문이다! 메모리 누수는 덤이다.

Comments