[전문가를 위한 C++] 14장. 에러 처리하기

Chapter 14. 에러 처리하기

14.1 에러와 예외

14.2 익셉션 처리 과정

  • 익셉션 객체는 항상 const 레퍼런스로 받는 것이 좋다. 익셉션 객체를 값으로 받으면 객체 슬라이싱이 발생한다.

    } catch (const exception& e) {
    
  • 여러 가지 익셉션 던지고 받기
    • exception을 상속한 runtime_error로 구현해 보자. 이 타입은 생성자를 호출할 때 예외에 대한 설명을 지정할 수 있다. runtime_error 익셉션 클래스는 <stdexcept> 헤더에 정의돼 있다.

      // 아래와 같이 2가지 상황에서 익셉션을 던졌다고 가정하자.
      throw runtime_error("Unable to open the file")
      throw runtime_error("Error reading the file")
      
      // main함수 : catch 구문이 runtime_error의 베이스 클래스인 exception 타입을 받도록 구현했다.
      // 따라서 위 2가지 에러를 모두 처리할 수 있다.
      try{
        myInt = readInteger(fileName);
      } catch (const exception& e) {
        cerr << e.what() << endl;
      }
      
    • 모든 익셉션 매칭하기

      • catch 문에서 모든 종류의 익셉션에 매칭하려면 다음과 같이 특수한 문법으로 작성한다.

         } catch (...) {
          cerr << "Error reading or opening file" << fileName << endl;
         }
        
  • noexcept
    • noexcept 키워드가 지정된 함수는 익셉션을 던지지 않는다.
    • 파생 클래스에서 virtual 메서드를 오버라이드할 때 베이스 클래스에 정의된 메서드에 noexcept가 지정되지 않았어도 오버라이드하는 메서드에 noexcept를 지정할 수 있다. 하지만 그 반대로는 할 수 없다.

14.3 익셉션과 다형성

  • 익셉션 클래스 직접 정의하기
    • 익셉션을 직접 정의할 때는 반드시 표준 exception 클래스를 직접 또는 간접적으로 상속하는 것이 좋다.

       class FileError : public exception {
        public:
          FileError(string_view fileName) : mFileName(fileName) {}
          virtual const char* what() const noexcept override {
            return mMessage.cstr();
          }
          string_view getFileName() const noexcept { return mFileName};
        protected:
          void setMessage(string_view message) { mMessage = message;}
        private:
          string mFileName;
          string mMessage;
       }
      
    • 익셉션으로 사용할 클래스를 정의할 때는 객체를 복제하거나 이동할 수 있도록 소멸자, 복제 생성자, 대입 연산자 그리고 이동 생성자와 이동 대입 연산자를 함께 정의해야 한다.
    • 익셉션을 레퍼런스로 받으면 쓸데 없는 복제 연산을 방지할 수 있다.
  • 중첩된 익셉션
    • 어떤 익셉션을 처리하는 catch문 안에서 새로운 익셉션을 던지고 싶다면 std::throw_with_nested()를 사용하면 된다. 나중에 발생한 익셉션을 처리하는 catch문에서 먼저 발생했던 익셉션에 접근할 때는 dynamic_cast()를 이용하면 된다. 이 때 먼저 발생한 익셉션은 nested_exception으로 표현한다.

14.4 익셉션 다시 던지기

  • 익셉션을 다시 던질 때는 반드시 throw;로 적어야 한다. e라는 익셉션을 던지기 위해 throw e;와 같이 작성하면 안 된다.

14.5 스택 풀기와 청소

  • 익셉션 처리 구문을 작성할 때 메모리나 리소스 누수가 발생하지 않도록 주의한다.
    • catch 핸들러를 발견하면 그 핸들러가 정의된 스택 단계로 돌아가는데, 이 과정에서 중간 단계에 있던 스택 프레임을 모두 풀어버린다.
    • 스코프가 로컬인 소멸자를 모두 호출하고, 각 함수에서 미처 실행하지 못한 코드는 건너뛴다.
    • 그런데 스택풀기가 발생할 때 포인터 변수를 해제하고 리소스를 정리하는 작업은 하지 않는다.
    • 따라서 C++로 코드를 작성할 때는 반드시 스택 기반 할당 방식을 이용하거나 다음 두 가지 방식 중 하나를 활용한다.

      void funcOne();
      void funcTwo();
      
      int main() {
        try {
          funcOne();
        } catch(const exception& e) {
          cerr << "Exception caught!" << endl;
          return 1;
        }
        return 0;
      }
      
      void funcOne() {
        string s1;
        string *s2 = new string();  // throw문이 실행되면 스택이 풀어져 해제되지 않는다. 메모리 릭!
        funcTwo();
        delete s2;  // 실행되지 않는다.
      }
      
      void funcTwo() {
        ifstream fileStream;  // 다행히 throw문이 실행되도 ifstream은 로컬변수이므로 소멸자는 호출된다.
        fileStream.open("filename");
        throw exception();
        fileStream.close();  // 실행되지 않는다.
      }
      

      스마트 포인터 활용

  • 스마트 포인터를 사용하면 funcOne()을 호출한 후 리턴될 때 혹은 그 안에서 익셉션이 발생할 때 자동으로 제거된다.

    void funcOne() {
      string s1;
      auto s2 = make_unique<string>("hello");
      funcTwo();
    }
    

익셉션 잡고, 리소스 정리한 뒤, 익셉션 다시 던지기

  • 각 함수마다 발생 가능한 익셉션을 모두 잡아서 리소스를 제대로 정리한 뒤 그 익셉션을 다시 스택의 상위 핸들러로 던지는 것이다.

    void funcOne() {
      string s1;
      string s2 = new string();
      try {
        funcTwo();
      } catch (...) {
        delete s2;  // 익셉션 처리 과정에서 호출
        throw;  // 잡은 익셉션을 다시 위로 던진다.
      }
      delete s2;  // 함수 정상 종료 시 호출
    }
    

14.6 익셉션 처리 과정에서 흔히 발생하는 문제

메모리 할당 에러

  • 시간될 때 다시 읽어보기

생성자에서 발생하는 에러

  • 생성자가 값을 리턴하지 못해도 익셉션을 던질 수는 있다. 익셉션을 활용하면 클라이언트가 객체의 정상 생성 여부를 쉽게 알 수 있다.
  • 하지만 익셉션이 발생해서 생성자가 정상 종료되지 않고 중간에 빠져나와버리면 객체의 소멸자가 호출될 수 없는 심각한 문제가 있다.
  • 따라서 익셉션이 발생해서 생성자를 빠져나올 때는 반드시 생성자에서 할당한 메모리와 리소스를 정리해야 한다.
    • 물론 일반 함수에서도 이 점은 주의해야 한다.

생성자를 위한 함수 try 블록

  • 생성자 이니셜라이저에서 발생한 익셉셔을 처리하기 위함.
  • 함수 try 블록은 사용하지 않는 것이 좋다!

소멸자에서 익셉션을 처리하는 방법

  • 소멸자에서 호출한 함수나 메서드에서 발생한 익셉션을 잡아서 처리할 때 조심하자. 소멸자에서 발생하는 에러는 반드시 소멸자 안에서 처리해야 한다. 익셉션을 다른 곳으로 보내면 안 된다.