CPP/씹어먹는 c++

[cpp개념공부] throw ,try-catch

뜨거운 개발자 2023. 2. 18. 16:08

throw : 예외 발생시키기

C언어와 다른 C++의 예외 처리

C 언어에서는 예외가 발생했을 때, 다른 값을 리턴하는 것으로 예외를 처리하였지만, C++ 에서는 예외가 발생하였다는 사실을 명시적으로 나타낼 수 있습니다. 바로 throw 문을 사용하면 됩니다.

표준 라이브러리 정의 예외 처리 객체

C++ 에는 예외를 던지고 싶다면, throw 로 예외로 전달하고 싶은 객체를 써주면 됩니다. 예외로 아무 객체나 던져도 상관 없지만, C++ 표준 라이브러리에는 이미 여러가지 종류의 예외들이 정의되어 있어서 이를 활용하는 것도 좋습니다.

예를 들어서, 위 경우 out_of_range 객체를 throw 합니다. C++ 표준에 out_of_range 외에 overflow_error, length_error, runtime_error 등등 여러가지가 정의되어 있고 표준 라이브러리에서 활용되고 있습니다.

throw 사용시 작동 과정

이렇게 예외를 throw 하게 되면, throw 한 위치에서 즉시 함수가 종료되고, 예외 처리하는 부분까지 점프하게 됩니다. 따라서 throw 밑에 있는 모든 문장은 실행되지 않습니다.

함수에서 예외 처리하는 부분에 도달하기 까지 함수를 빠져나가면서, stack 에 생성되었던 객체들을 빠짐없이 소멸시켜 줍니다.

따라서 예외가 발생하여도 사용하고 있는 자원들을 소멸자만 제대로 작성하였다면 제대로 소멸시킬 수 있습니다!

예시코드

const T& at(size_t index) const {
    if (index >= size_) {
      throw out_of_range("vector 의 index 가 범위를 초과하였습니다.");
    }
    return data_[index];
  }

이렇게 예외가 발생하는 상황에 throw 함수를 사용해서 out_of_range라는 C++표준에 해당하는 객체들을 던져줍니다.

예외 처리하기 try와 catch

#include <iostream>
#include <stdexcept>

template <typename T>
class Vector {
 public:
  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 std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
    }
    return data_[index];
  }
  ~Vector() { delete[] data_; }

 private:
  T* data_;
  size_t size_;
};
int main() {
  Vector<int> vec(3);

  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문 안에서 무언가 예외가 발생할 만한 코드가 실행이 됩니다.

즉 예외가 발생할 수 있는 상황을 적어두고 그곳에 예외가 발생하면 throw로 그 예외를 보내는 겁니다.

예외가 발생하지 않은경우

만약 발생하지 않았다면 try 밑부분에 있는 catch 부분이 없는 것과 동일하게 실행 됩니다.

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

위의 예시 코드에서는 data 에는 vec 의 index 번째 값이 들어가고 밑에 있는 catch 문은 무시 됩니다.

예외가 발생한 경우

예외가 발생하게되면, 그 즉시 stack 에 생성된 모든 객체들의 소멸자들이 호출되고, 가장 가까운 catch 문으로 점프합니다.

catch

catch 문은 throw 된 예외를 받는 부분인데, catch 문 안에 정의된 예외의 꼴에 맞는 객체를 받게 됩니다.

사용 예시

즉 이해가 쉽도록 예시를 들자면 throw std::out_of_range("error"); 이렇게 적어주게 된다면 받는 catch문은 catch (std::out_of_range& e) 이렇게 받는 객체를 catch문안에 명시를 해주면 됩니다.

out_of_range

out_of_range 클래스는 아주 간단한데, 내부에 발생예외에 대한 문자열 필드만 하나 있고, what() 멤버 함수로 그 값을 볼 수가 있습니다.

스택 풀기 (stack unwinding)

예제

코드

#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!

해석

이 예제에서 throw를 하고 catch문으로 가는 과정에서 모든 스택의 리소스가 해제 되어서 catch에 도달하기 전 다 해제가 되는 걸 볼 수가 있다.

여기서 해제는 마지막으로 만든 부분부터 가장 먼저 만든 부분 순서대로 해제가 되는 걸 볼 수 가 있다.

catch로 점프 하면서 스택 상에서 정의된 객체들을 소멸시키는 과정을 스택 풀기(stack unwinding)이라고 부릅니다.

주의사항 : 예외를 생성자에서 던지는 경우

예외를 생성자에서 던질 때 주의해야 할 점이 하나 있습니다. 바로 생성자에서 예외가 발생 시에 소멸자가 호출되지 않는다는 점입니다. 따라서, 만일 예외를 던지기 이전에 획득한 자원이 있다면 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;
  } 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;
  }
}

실행 결과

이렇게 throw하는 객체의 종류에 따라서 실행되는 결과 값이 다름을 알 수가 있다.

상속관계에서 throw

예시 코드

#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를 입력했을 때도 Parent catch가 나온다는게 이상하다. 그 이유는catch문의 경우 가장 먼저 대입될 수 있는 객체를 받는데 Parent& p = Child(); 즉 업캐스팅이 가능하기 때문에 Catch문에서 최상위 부모가 먼저 받아버리는 형태가 발생하는 것이다.

따라서 이걸 방지해주려면 Parent 형태의 catch를 child보다 뒤에 써 주는 것이 좋다.(다운 캐스팅이 불가능하기 때문) (Child &c = Parent(); // 오류)

변경 결과

위와 같이 나오는 것을 볼 수가 있다.

따라서 catch의 순서만 바꿔서 실행을 해보면

예외 객체 상속

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

모든 예외 받기

throw의 적절한 catch가 없는 경우

만약에 어떤 예외를 throw하였는데, 이를 받는 catch가 없다면 어떻게 될까요?

runtime_error예외를 발생시키며 프로그램이 비정상적으로 종료되었다고 뜨게 됩니다. 따라서, 언제나 예외를 던지는 코드가 있다면 적절하게 받아내는 것이 중요합니다.

모든 예외 처리 catch catch(…)

우리는 switch 에서 이외의 모든 케이스를 default를 통해 받은 것 처럼 c++ 에서도 이외의 모든 케이스에 대해서 catch하는 catch문이 있습니다.

catch(...) 에서 try안에서 발생한 모든 예외들을 받게 됩니다.

#include <iostream>
#include <stdexcept>

int func(int c) {
  if (c == 1) {
    throw 1;
  } else if (c == 2) {
    throw "hi";
  } else if (c == 3) {
    throw std::runtime_error("error");
  }
  return 0;
}

int main() {
  int c;
  std::cin >> c;

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

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

만약에 어떤 함수가 예외를 발생시키지 않는다면 noexcept 를 통해 명시할 수 있습니다.

int foo() noexcept {} 이렇게 적으면 foo함수는 예외를 발생시키지 않는다고 표기를 한 것 입니다.

컴파일러 입장에서 noexcept

다만 noexcept키워드를 붙혔다고 해서 함수가 예외를 절대 안 던지는 것은 아닙니다. 실제로 noexcept키워드를 붙히고 예외를 던지는 경우가 있을 수는 있습니다.(컴파일러가 경고를 하긴 합니다.) 다만 컴파일러는 noexcept 키워드가 붙었으니까 예외를 발생시키지 않는다고 믿고 그대로 컴파일 하게 됩니다.

noexcept에서 예외 발

그래서 noexcept가 명시된 함수에서 throw를 하게 된다면 예외가 처리가 되는 것이 아니라 프로그램이 종료가 됩니다. (abort가 날겁니다.)

c++11 소멸자

C++ 11 에서 부터 소멸자들은 기본적으로 noexcept입니다. 절대로 소멸자에서 예외를 던지면 안됩니다.


Uploaded by N2T

728x90