예외, try & catch, noexcept

하루공부·2024년 1월 19일
0

C++

목록 보기
9/25
post-thumbnail

C++ 아이콘 제작자: Darius Dan - Flaticon


예외

  • 예외란? 사람은 실수하기 마련이고 또 컴퓨터는 필요한 자원을 무조건 제공할 수 있는 것은 아니다.
std::vector<int> v(3); // 크기가 3 인 벡터 만듦
std::cout << v.at(4); // ?? ==> 오류발생
std::vector<int> v(1000000000); ==> 평범한 시스템의 경우 이렇게 큰 메모리를 할당할 수 없음
  • 이렇게 정상적인 상황에서 벗어난 모든 예외적인 상황들을 예외(exception)이다.

  • c언어에선 문제가 생길만한 곳에 if문으로 return하는 것으로 예외를 처리했지만 c++은 명시적으로 예외가 발생했음을 표시할 수 있다. ==> throw

    간단하게 백터 클래스를 만든다고 생각하자
    at함수를 만드는데 접근하려는 범위가 해당 벡터 크기 범위 이내라면 data[index] 그냥 리턴하면 되지만
    범위 밖이라면??
    문제는 at함수는 리턴값이 const T& 이라서 따로 리턴하는 것으로 처리 불가능

    throw로 명시적으로 알린다.

    if (index >= size) 
       throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다."); 
  • C++ 에는 예외를 던지고 싶다면, throw로 예외로 전달하고 싶은 객체를 써주면 됩니다.

    예외로 아무 객체나 던져도 상관 없지만 C++ 표준 라이브러리에는 여러가지 종류의 예외들이 정의되어 있다.

이렇게 예외를 throw 하게 되면, throw 한 위치에서 즉시 함수가 종료되고
예외 처리하는 부분까지 점프한다. 그러면 throw 밑의 로직은 실행되지 않음.

  • 여기서 예외를 처리하는 부분에 도달하기 까지 함수를 빠져나가면서 stack에 생성된 객체들을 전부 소멸시킨다. ==> 대신 소멸자를 제대로 작성해야함

  • 그럼 예외를 던지고 그것을 처리하는 것은 어떻게 하냐? ==> catch



try 와 catch

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

우선 try 부분은 예외가 발생할 만한 지역을 지정한다.
catch는 예외가 발생되면 실행되는 코드 ==> 예외를 처리한다.

catch 블록은 항상 try 블록의 뒤에 이어서 등장해야 하며, try 블록에서 발생한 예외는 catch 블록에서 처리된다.
만약 예외가 일어나지 않는다면 그냥 try ... catch 부분이 없는 것처럼 실행된다.


  • 예외 처리는 어떻게 이루어 지는가?
    • 예외가 발생한다면 위에서 말한 stack에 생성된 모든 객체의 소멸자가 호출되고 (이와 같이 catch 로 점프 하면서 스택 상에서 정의된 객체들을 소멸시키는 과정을 스택 풀기(stackunwinding) 이라고 한다.)
      가장 가까운 catch문으로 점프한다.

      위의 경우 throw 다음 아래 로직이 실행된다.

      catch (std::out_of_range& e) {std::cout << "예외 발생 ! " << e.what() << std::endl;

      여기서 catch 문은 throw에서 던진 객체에 맞는 객체를 받는다.
      위에서 throw std::out_of_range("vector 의 index 가 범위를 초과하였습니다.");
      out_of_range 를 throw 하였는데, 위 catch 문이 out_of_range를 받으므로, 잘 처리한다.


  • 1가지 주의할 점이 있다. 생성자에서 예외가 발생하면 소멸자가 호출되지 않는다.
    ==> 만약 발생한다면 흭득한 자원에 대해서 catch에서 잘 해제해야 한다.


  • catch는 여러 종류의 예외를 받을 수 있고 try뒤에 catch를 여러개 달 수 있다.
    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!";
    }
    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;
    }


  • 상속을 받는 클래스에서 처리하는 방식이 따로 있다.

    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!  Parent! 출력
    }        // 2입력시 -> Parent Catch!  Child! 출력

    Parent 클래스 객체를 throw할 때는 잘 작동하는데 Child 객체는 이상하다.

    • 이와 같은 일이 발생한 이유는 catch 문의 경우 가장 먼저 대입될 수 있는 객체를 받기 때문이다.

      Parent& p = Child(); 는 가능하기 때문에 Parent catch 가 먼저 받아버리는 것

      • 위와 같은 문제를 방지하기 위해서는 언제나 Parent catch 를 Child catch 보다 뒤에 써야한다.
      • 왜냐하면 이를 통해서 Child 객체가 Parent catch 에 들어가는 것을 막을 수 있고
        Child &c = Parent(); // 오류
      • 위는 성립되지 않기 때문에 Child catch 에 Parent 객체가 들어가지도 않는다.
  • 일반적으로 예외 객체는 std::exception 을 상속 받는 것이 좋다. 왜냐면 유용한 함수들이 있어서...



  • 만약 throw했는데 이를 받을 catch가 없다면??

    switch문의 default랑 if-else문에서 마지막 else와 같은 기능이 존재한다.
    이 기능을 사용하자. 대신 어떤 예외도 다 받기에 특정한 타입에 대해 적용X




noexcept

  • 만약에 어떤 함수가 예외를 발생시키지 않는다면 noexcept 를 통해 명시 가능하다.
  • noexcept 키워드를 붙였다고 해서 함수가 예외를 절대로 던지지 않는다는 것은 아니다.
    ex) int func() noexcept {}
    • 컴파일러는 noexcept 키워드가 붙은 함수가 예외를 발생시키지 않는다는 것을 믿고
      그대로 컴파일 한다
    • 대신 noexcept 로 명시된 함수에서 예외가 발생되면 예외가 제대로 처리되지 않고 프로그램이 종료돤다.
  • 그럼 왜 사용하냐??
    ==> 단순히 프로그래머가 컴파일러에게 주는 힌트
    ==> 컴파일러가 절대로 예외를 발생시키지 않는다는 사실을 안다면, 여러가지 추가적인 최적화를 수행
    ==> C++ 11 에서 부터 소멸자들은 기본적으로 noexcept 소멸자에서 절대로 예외를 던지지 말자.

참조
공부한 내용 복습

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

0개의 댓글