와와

[ 씹어먹는 C++ ] C++에서의 예외 처리 본문

개발/C++

[ 씹어먹는 C++ ] C++에서의 예외 처리

정으주 2024. 9. 8. 14:31

C++에서의 예외 처리

 

예외 발생시키기 - throw

C++ 에서는 throw를 통해 예외가 발생하였다는 것을 명시적으로 나타낼 수 있다.

 

위과 같이 throw 로 예외로 전달하고 싶은 객체를 써주면 된다.

C++ 표준에는 out_of_range, overflow_error, length_error, runtime_error 등등 여러가지가 정의되어 있고 표준 라이브러리에서 활용한다.

 

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

예외 throw -> 즉시 함수 종료 -> 예외 처리하는 부분까지 점프 (catch)

 

의 과정을 거치므로 throw 밑에 있는 모든 문장은 실행되지 않는다.

이렇게 함수에서 예외 처리하는 부분에 도달하기 까지 함수를 빠져나가면서, stack 에 생성되었던 객체들을 빠짐없이 소멸시켜 주는데, 덕분에 예외가 발생하여도 사용하고 있는 자원들을 제대로 소멸시킬 수 있다. (소멸자만 제대로 작성하였다면)

 

 

try-catch

 

 

 

여기서 catch 문은 throw 된 예외를 받는 부분이다.

catch 문 안에 정의된 예외의 꼴에 맞는 객체를 받게 된다.

위의 Vector 의 경우 out_of_range 를 throw 하였으니 catch 문에서는 out_of_range 를 받아야 한다.

 

스택 풀기 (stack unwinding)

throw가 발생했을 때 catch 로 점프 하면서 스택 상에서 정의된 객체들을 소멸시키는 과정을 스택 풀기(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!

 

단, 생성자에서 예외가 발생 시에 소멸자가 호출되지 않는다. 따라서, 만일 예외를 던지기 이전에 획득한 자원이
있다면 catch 에서 잘 해제시켜 줘야만 한다.

 

 

여러 종류의 예외 받기

 

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

 

이렇게 throw 를 여러번 던지고, 여러개의 catch 로 받을 수 있다.

파생 클래스를 사용할 경우 기반 클래스 겹치지 않도록 주의해야함.

( 위와 같은 상황에선 Child 를 catch하는 코드가 불리지 않는다. Parent catch 를 Child catch 보다 뒤에 써주는 것이 좋다. )

 

 

모든 예외 받기

 

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

 

 

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

int foo() noexcept {}

 

컴파일러는 noexcept 키워드가 붙은 함수는 예외를 발생시키지 않는구나 라고 곧이곧대로 믿고, 그대로 컴파일 하게 된다.대신 noexcept 로 명시된 함수가 예외를 발생시키게 된다면 예외가 제대로 처리되지 않고 프로그램이 종료된다. 컴파일러가 어떤 함수가 절대로 예외를 발생시키지 않는다는 사실을 안다면, 여러가지 추가적인 최적화를 수행할 수 있다.

 

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

 

https://cplusplus.com/reference/stdexcept/

 

https://cplusplus.com/reference/stdexcept/

 

cplusplus.com