Program Language/C & C++

[C++] Catching Exception

야곰야곰+책벌레 2022. 1. 13. 16:26
728x90
반응형
void f()
{
	try {
		throw E{};
	}
	catch(H) {
		// when do we get here?
	}
}

핸들러는 호출된다.

  1. H가 E와 같은 유형인 경우
  2. H가 E의 명확한 공개 기반인 경우
  3. H와 E가 포인터 유형이고 [1] 또는 [2]가 참조하는 유형에 대해 유지되는 경우
  4. H가 참조이고 [1] 또는 [2]가 H가 참조하는 유형에 대해 유지되는 경우

또한 예외를 잡는 데 사용되는 유형에 const를 추가할 수 있다. 이것은 우리가 잡을 수 있는 예외 집합을 변경하지 않는다. catch 된 예외를 수정하지 못하도록 제한할 뿐이다.

참조로 예외를 catch 할 가능성에 유의하자. 예외 유형은 종종 그들이 나타내는 오류 종류 간의 관계를 반영하기 위해 클래스 계층의 일부로 정의된다. 예외 클래스를 계층 구조로 구성하는 기술은 일부 프로그래머가 모든 예외를 참조로 잡는 것을 선호하기에 충분히 일반적이다.

try-part 및 catch-clause의 try-block 모두에 있는 {}는 실제 범위다. 결과적으로 이름이 try-block의 두 부분 또는 외부에서 사용되는 경우 해당 이름은 try-block 외부에서 선언되어야 한다.

void g()
{
	int x1;
	try {
		int x2 = x1;
		// ...
	}
	catch (Error) {
		++x1; // OK
		++x2; // error : x2 not in scope
		int x3 = 7;
		// ...
	}
	catch(...) {
		++x3; // error : x3 not in scope
		// ...
	}
    
	++x1; // OK
	++x2; // error : x2 not in scope
	++x3; // error : x3 not in scope
}

Rethrow

예외를 포착하면 핸들러가 오류를 완전히 처리할 수 없다고 결정하는 것이 일반적이다. 이 경우 핸들러는 일반적으로 로컬에서 수행할 수 있는 작업을 수행한 다음 예외를 다시 throw 한다. 따라서 가장 적절한 위치에서 오류를 처리할 수 있다. 이는 오류를 가장 잘 처리하는 데 필요한 정보를 한 곳에서 사용할 수 없어 복구 작업이 어려 핸들러에 가장 잘 분산되는 경우에도 마찬가지다.

void h()
{
	try {
		// ... code that might throw an exception ...
	}
	catch (std::exception& err) {
		if (can_handle_it_completely) {
			// ... handle it ...
			return;
		}
		else {
			// ... do what can be done here ...
			throw; // rethrow the exception
		}
	}
}

rethrow는 피연산자가 없는 throw로 표시된다. rethrow는 catch절 또는 chatch절에서 호출된 함수에서 발생할 수 있다. rethrow에 대한 예외가 없을 때 rethrow가 시도되면 std::terminate()가 호출된다. 컴파일러는 이러한 경우 전체가 아닌 일부를 감지하고 경고할 수 있다.

다시 throw 된 예외는 포착된 원래 예외이며 예외로 액세스 할 수 있었던 부분만이 아니다. 예를 들어 out_of_range가 던져진 경우 h()는 그것을 일반 예외로 catch 하지만 throw 한다. 여전히 out_of_range로 rethrow 될 것이다. 

모든 예외 catch

<stdexcept>에서 표준 라이브러리는 공통 기본 exception과 함께 예외 클래스의 작은 계층 구조를 제공한다.

void m()
{
	try {
		// ... do something ...
	}
	catch (std::exception& err) { // handle every standard-librar y exception
		// ... cleanup ...
		throw;
	}
}

이것은 모든 표준 라이브러리 예외를 포착한다. 그러나 표준 라이브러리 예외는 예외 유형의 한 세트일 뿐이다. 결과적으로 std::exception의 catch로 모든 예외를 잡을 순 없다. 누군가 일부 응용 프로그램별 계층에서 int 또는 예외를 던진 경우 std::exception&에 대한 핸들러가 이를 catch하지 않는다.

그러나 종종 모든 종류의 예외를 처리해야 한다. 예를 들어, m()이 포인터를 발견한 상태로 일부 포인터를 남겨둔다고 가장하면 핸들러에 코드를 작성하여 허용 가능한 값을 제공할 수 있다. 함수의 경우 줄임표... 는 '모든 인수'를 나타내므로 catch(...)는 '예외 catch'를 의미한다.

void m()
{
	try {
		// ... something ...
	}
	catch (...) { // handle every exception
		// ... cleanup ...
		throw;
	}
}

다중 핸들러

try-block에는 여러 catch-clause(핸들러)가 있을 수 있다. 파생된 예외는 둘 이상의 예외 유형에 대한 처리기에 의해 catch 될 수 있으므로 처리기가 try문에서 작성되는 순서가 중요하다. 핸들러는 순서대로 시도된다.

void f()
{
	try {
		// ...
	}
	catch (std::ios_base::failure) {
		// ... handle any iostream error (§30.4.1.1) ...
	}
	catch (std::exception& e) {
		// ... handle any standard-librar y exception (§30.4.1.1) ...
	}
	catch (...) {
		// ... handle any other exception (§13.5.2.2) ...
	}
}

컴파일러는 클래스 계층 구조를 알고 있으므로 많은 논리적 실수에 대해 경고할 수 있다.

void g()
{
	try {
		// ...
	}
	catch (...) {
		// ... handle every exception (§13.5.2.2) ...
	}
	catch (std::exception& e) {
		// ...handle any standard librar y exception (§30.4.1.1) ...
	}
	catch (std::bad_cast) {
		// ... handle dynamic_cast failure (§22.2.1) ...
	}
}

여기서 예외는 고려되지 않는다. catch-all 핸들러를 제거하더라도 exception에서 파생되었기 때문에 bad_cast는 고려되지 않는다. catch-clauses에 대한 예외 유형 일치는 런타임 작업이며 오버로드 해결만큼 일반적이지 않다.

함수 try-block

함수의 본문은 try블록이 될 수 있다.

int main()
{
	try
	{
		// ... do something ...
	}
	catch (...} {
		// ... handle exception ...
	}
}

대부분의 함수에서 try-block함수를 사용하여 얻을 수 이는 것은 약간의 표기상의 편리함 뿐이다. 그러나 try-block을 사용하면 생성자에서 기본 또는 멤버 intializer가 throw 하는 예외를 처리할 수 있다. 기본적으로 기본 또는 멤버 intializer에서 예외가 발생하면 멤버 클래스의 생성자를 호출한 모든 항목에 예외가 전달된다. 그러나 생성자 자체는 멤버 initializer 목록을 포함하여 전체 함수 본문을 try-block으로 묶음으로써 이러한 예외를 포착할 수 있다.

class X {
	vector<int> vi;
	vector<string> vs;
	// ...
public:
	X(int,int);
	// ...
};

X::X(int sz1, int sz2)
{
	try
		:vi(sz1), // construct vi with sz1 ints
		vs(sz2), // construct vs with sz2 strings
	{
		// ...
	}
	catch (std::exception& err) { // exceptions thrown for vi and vs are caught here
		// ...
	}
}

그래서 우리는 멤버 생성자에 의해 던져진 예외를 잡을 수 있다. 마찬가지로 소멸자에서 멤버 소멸자가 throw 한 예외를 catch 할 수 있다.(소멸자는 절대 throw 해서는 안됨). 그러나 예외가 발생하지 않은 것처럼 객체를 복구할 수 없고 정상적으로 반환할 수 없다. 멤버 생성자의 예외는 멤버가 유효한 상태가 아닐 수 있음을 의미한다. 또한 다른 멤버 객체는 생성되지 않았거나 스택 해제의 일부로 소멸자가 이미 호출되었다.

생성자나 소멸자에 대한 try-block 함수의 catch-clause에서 할 수 있는 최선은 예외를 던지는 것이다. 기본 동작은 catch-clause의 "fall off the end" 때 원래 예외를 다시 throw 하는 것이다.

일반 함수의 try-block에는 이러한 제한이 없다.

Termination

덜 미묘한 오류 처리 기술을 위해 예외 처리를 포기해야 하는 경우가 있다. 이를 위한 원칙은 다음과 같다.

  • 예외를 처리하는 동안 예외를 throw하지 마라
  • catch 할 수 없는 예외를 throw 하지 마라

예외 처리 구현이 둘 중 하나를 수행하는 것을 포착하면 프로그램이 종료된다.

한 번에 두 개의 예외를 활성화할 수 있었다면 (동일한 스레드에서, 그렇게 할 수 없음) 시스템은 처리하려고 시도할 예외가 무엇인지 알 수 없다. (새로운 것 도는 이미 처리하려고 했던 것) 예외는 catch절에 들어가는 즉시 처리되는 것으로 간주된다. 예외를 다시 throw 하거나 catch절 내에서 새 예외를 throw 하는 것은 원래 예외가 처리된 후에 수행된 새로운 throw로 간주된다. 소멸자를 떠나기 전에 catch 하는 한 소멸자 내에서 예외를 throw 할 수 있다. (스택 해제 중에도 포함)

terminate() 호출에 대한 특정 규칙은 아래와 같다.

  • throw 된 예외에 대해 적절한 핸들러를 찾지 못한 경우
  • noexcept함수가 throw와 함께 종료하려고 할 때
  • 스택 해제 중에 호출된 소멸자가 throw로 종료하려고 할 때
  • 예외를 전파하기 위해 호출된 코드(예:복사 생성자)가 throw와 함께 종료하려고 할 때
  • 현재 처리 중인 예외가 없을 때 누군가가 다시 누군가가 다시 throw 하려고 할 때
  • 정적으로 할당된 또는 thread 로컬 객체에 대한 소멸자가 throw와 함께 종료하려고 할 때
  • 정적으로 할당된 또는 thread 로컬 객체에 대한 초기화 프로그램이 throw와 함께 종료하려고 할 때
  • atexit() 함수로 호출된 함수가 throw와 함께 종료하려고 할 때

이러한 경우 std::terminate() 함수가 호출된다. 또한, 덜 과감한 접근 방식이 실행 불가능한 경우 사용자는 terminate()를 호출할 수 있다.

'tries to exit with a throw, '는 런타임 시스템이 함수에서 호출자에게 예외를 전파하려고 시도하도록 예외 어딘가에 throw 되고 catch 되지 않음을 의미한다.

기본적으로 terminate()는 abort()를 호출한다. 이 기본값은 특히 디버깅 중에 대부분의 사용자에게 올바른 선택이다. 이것이 허용되지 않는 경우 사용자는 <exception>에서 std::set_terminate()를 호출하여 종료 처리기 함수를 제공할 수 있다.

using terminate_handler = void(∗)(); // from <exception>
[[noreturn]] void my_handler() // a ter minate handler cannot return
{
	// handle termination my way
}
void dangerous() // very!
{
	terminate_handler old = set_terminate(my_handler);
	// ...
	set_terminate(old); // restore the old terminate handler
}

반환 값은 set_terminate()에 제공된 이전 함수다.

예를 들어 terminate 핸들러를 사용하여 프로세스를 중단하거나 시스템을 다시 초기화할 수 있다. 그 의도는 예외 처리 메커니즘에 의해 구현된 오류 복구 전략이 실패하고 다른 수준의 내결함성 전략으로 이동할 때 적용되는 과감한 조치인 terminate()이다. terminate 핸들러가 입력되면 기본적으로 프로그램의 데이터 구조에 대해 아무것도 가정할 수 없다. 손상되었다고 가정해야 한다. cerr을 사용하여 오류 메시지를 작성하는 것조차 위험하다고 가정해야 한다. 또한 dangerous()가 작성되었으므로 예외로부터 안전하지 않다. set_terminate(old) 이전의 throw 또는 반환은 원래 의도되지 않은 위치에 my_handler를 남겨둔다. 

termniate 핸들러는 호출자에게 돌아갈 수 없다. 시도하면 terminate()가 abort()를 호출한다.

abort()는 프로그램에서 비정상적인 종료를 나타낸다. exit() 함수는 종료가 정상인지 비정상인지 주변 시스템에 나타내는 반환 값으로 프로그램을 종료하는 데 사용할 수 있다.

포착되지 않은 예외로 인해 프로그램이 종료될 때 소멸자가 호출되는지 여부는 구현에서 정의된다. 일부 시스템에서는 디버거에서 프로그램을 재개할 수 있도록 소멸자를 호출하지 않는 것이 중요하다. 다른 시스템에서는 핸들러를 검색하는 동안 소멸자를 호출하지 않는 것이 구조적으로 불가능에 가깝다.

잡히지 않는 예외가 발생했을 때 정리를 확실히 하려면 관심 있는 예외 대한 핸들러 외에 catch-all 핸들러를 main()에 추가할 수 있다.

int main()
{
	try {
		// ...
	}
	catch (const My_error& err) {
		// ... handle my error ...
	}
	catch (const std::range_error&)
	{
		cerr << "range error: Not again!\n";
	}
	catch (const std::bad_alloc&)
	{
		cerr << "new ran out of memory\n";
	}
	catch (...) {
		// ...
	}
}

이것은 네임스페이스 및 스레드 로컬 변수의 생성 및 파괴로 인해 발생한 예외를 제외한 모든 예외를 catch 한다. 네임스페이스 및 스레드 로컬 변수의 초기화 또는 소멸 중에 발생한 예외를 catch 할 방법이 없다. 이것이 가능할 때마다 전역 변수를 피해야 한느 또 다른 이유다.

예외가 catch 되면 일반적으로 예외가 발생한 정확한 지점을 알 수 없다. 이는 디버거가 프로그램 상태에 대해 알고 있는 정보와 비교하여 정보 손실을 나타낸다. 일부 C++ 개발 환경, 일부 프로그램 및 일부 사용자의 경우 프로그램이 복구하도록 설계되지 않은 예외를 catch하지 않는 것이 더 나을 수 있다.

728x90
반응형

'Program Language > C & C++' 카테고리의 다른 글

[C++] 클래스 explicit 생성자  (0) 2022.01.14
[C++] namespace를 이용한 version 관리  (0) 2022.01.14
[C++] throwing exception  (0) 2022.01.13
[C++] Finally  (0) 2022.01.13
[C++] 리소스 관리  (0) 2022.01.13