catch
catch (예외 처리기)
throw로 던져진 예외는 결국 어딘가에서 잡혀야 해요. 그 예외를 잡아 처리하는 것이 바로 핸들러(handler), 즉 catch 블록이에요. 예외는 핸들러에 의해 처리될 수 있어요.
출처: cppreference
본문
핸들러(Handler)
catch ( 속성(optional) type-specifier-seq declarator ) compound-statement |
(1) | |
catch ( 속성(optional) type-specifier-seq abstract-declarator(optional) ) compound-statement |
(2) | |
catch ( ... ) compound-statement |
(3) |
| 자리 | 의미 |
|---|---|
| 속성(attr) | (C++11부터) 임의 개수의 속성, 매개변수에 적용됨 |
| type-specifier-seq | 형식 매개변수 선언의 일부, 함수 매개변수 목록에서와 동일 |
| declarator | 매개변수 선언의 일부, 함수 매개변수 목록에서와 동일 |
| abstract-declarator | 이름 없는 매개변수 선언의 일부, 함수 매개변수 목록에서와 동일 |
| compound-statement | 복합 문 |
핸들러의 매개변수 선언은 그 핸들러에 진입하게 만들 수 있는 예외의 타입(들)을 설명해요.
매개변수가 다음 타입 중 하나로 선언되면 프로그램은 잘못된 형태(ill-formed)예요.
- 불완전 타입(incomplete type)
- 추상 클래스 타입
- rvalue 참조 타입 (C++11부터)
- (cv-한정될 수 있는)
void를 제외한 불완전 타입에 대한 포인터 - 불완전 타입에 대한 lvalue 참조
매개변수가 "T의 배열" 타입이나 함수 타입 T로 선언되면, 그 타입은 "T에 대한 포인터"로 조정돼요.
매개변수 타입 T를 가진 핸들러는 "T 타입의 핸들러"라고 줄여 부를 수 있어요.
예외 매칭(Matching exceptions)
각 try 블록은 여러 핸들러와 연결되는데, 이 핸들러들은 **핸들러 시퀀스(handler sequence)**를 이뤄요. try 블록에서 예외가 던져지면, 시퀀스의 핸들러들이 나타난 순서대로 예외와 매치를 시도해요.
타입 E의 예외 객체에 대해 핸들러가 매치가 되려면 다음 조건 중 하나를 만족해야 해요.
- 핸들러의 타입이 "cv-한정될 수 있는
T" 또는 "cv-한정될 수 있는T에 대한 lvalue 참조"이고, 아래 조건 중 하나를 만족하는 경우:T와E가 같은 타입이거나,T가E의 (cv-한정될 수 있는) 기반 클래스인 경우
- 핸들러의 타입이 "cv-한정될 수 있는
T" 또는 "const T&"이고,T가 포인터나 멤버에 대한 포인터 타입이며, 아래 조건 중 하나를 만족하는 경우:- 포인터 타입 변환(함수 포인터 변환, C++17부터) 등으로
T와E가 변환 관계에 있는 경우 E가std::nullptr_t인 경우 (C++11부터)
- 포인터 타입 변환(함수 포인터 변환, C++17부터) 등으로
catch(...) 핸들러는 어떤 타입의 예외와도 매치돼요. 존재한다면 핸들러 시퀀스에서 마지막 핸들러여야만 해요. 이 핸들러는 nothrow 예외 보증을 제공하는 함수에서 잡히지 않은 예외가 빠져나가는 일을 막는 데 쓸 수 있어요.
try
{
f();
}
catch (const std::overflow_error& e)
{} // f()가 std::overflow_error를 던지면 실행 (같은 타입 규칙)
catch (const std::runtime_error& e)
{} // f()가 std::underflow_error를 던지면 실행 (기반 클래스 규칙)
catch (const std::exception& e)
{} // f()가 std::logic_error를 던지면 실행 (기반 클래스 규칙)
catch (...)
{} // f()가 std::string이나 int, 또는 그 외 무관한 타입을 던지면 실행
어떤 try 블록의 핸들러들 사이에서 매치를 찾지 못하면, 같은 스레드의 동적으로 둘러싸는 try 블록( C++11부터)에서 매치되는 핸들러를 계속 찾아요.
매치되는 핸들러를 끝내 찾지 못하면 std::terminate가 호출돼요. 이 std::terminate 호출 전에 스택이 풀리는지(unwound) 여부는 구현 정의예요.
예외 처리(Handling exceptions)
예외가 던져지면 제어는 타입이 매치되는 가장 가까운 핸들러로 옮겨져요. "가장 가깝다"는 것은, try 키워드 뒤에 오는 복합 문이나(있는 경우) 멤버 초기화 목록을 제어 스레드가 가장 최근에 진입했고 아직 빠져나오지 않은 핸들러를 의미해요.
핸들러 매개변수 초기화
매개변수 목록에 선언된 매개변수("cv-한정될 수 있는 T" 또는 "cv-한정될 수 있는 T에 대한 lvalue 참조" 타입)는 타입 E의 예외 객체로부터 다음과 같이 초기화돼요.
T가E의 기반 클래스이면, 매개변수는 예외 객체의 대응하는 기반 클래스 부분 객체를 나타내는 타입T의 lvalue로부터 복사 초기화돼요.- 그 외에는, 매개변수는 예외 객체를 나타내는 타입
E의 lvalue로부터 복사 초기화돼요.
매개변수의 수명은 핸들러가 종료될 때 끝나는데, 핸들러 안에서 초기화된 자동 저장 기간 객체들이 파괴된 뒤예요.
매개변수가 객체로 선언되면, 그 객체에 대한 변경은 예외 객체에 영향을 주지 않아요.
매개변수가 객체에 대한 참조로 선언되면, 참조된 객체에 대한 변경은 예외 객체에 대한 변경이고, 그 객체가 다시 던져지면(rethrown) 효과를 가져요.
핸들러 활성화(Activating the handler)
핸들러의 (있는 경우) 매개변수에 대한 초기화가 완료되면 핸들러가 활성(active) 상태로 간주돼요.
또한, 던지기(throw) 때문에 std::terminate에 진입하면 암시적 핸들러가 활성 상태로 간주돼요.
핸들러가 종료되면 더 이상 활성 상태로 간주되지 않아요.
가장 최근에 활성화됐고 여전히 활성 상태인 핸들러를 가진 예외를 **현재 처리되는 예외(currently handled exception)**라고 불러요. 이런 예외는 다시 던질 수 있어요.
제어 흐름(Control flow)
핸들러의 복합 문은 제어 흐름이 제한된 문(control-flow-limited statement)이에요.
void f()
{
goto label; // error
try
{
goto label; // error
}
catch (...)
{
goto label: // OK
label: ;
}
}
주의할 점(Notes)
제어가 핸들러로 옮겨지는 동안 스택 풀기(stack unwinding)가 일어나요. 핸들러가 활성화될 때는 스택 풀기가 이미 완료돼 있어요.
throw 0 표현식이 던지는 예외는 포인터나 멤버에 대한 포인터 타입의 핸들러와 매치되지 않아요.
그런 핸들러와 매치되는 널 포인터를 던지려면 throw nullptr을 쓸 수 있어요. (C++11부터)
예외 객체는 절대 배열이나 함수 타입을 가질 수 없으므로, 배열이나 함수 타입에 대한 참조형 핸들러는 어떤 예외 객체와도 매치되지 않아요.
절대 실행될 수 없는 핸들러를 작성할 수도 있어요. 예를 들어 최종 파생 클래스의 핸들러를 대응하는 모호하지 않은 public 기반 클래스의 핸들러 뒤에 두는 경우예요.
try
{
f();
}
catch (const std::exception& e)
{} // f()가 std::runtime_error를 던지면 실행될 것
catch (const std::runtime_error& e)
{} // dead code!
많은 구현이 CWG 이슈 388의 해결을 non-const 포인터 타입에 대한 참조 핸들러까지 과도하게 확장해요.
int i;
try
{
try
{
throw static_cast<float*>(nullptr);
}
catch (void*& pv)
{
pv = &i;
throw;
}
}
catch (const float* pf)
{
assert(pf == nullptr); // 통과해야 하지만 MSVC와 Clang에선 실패
}
예제
여기서는 값으로 잡은 예외와 기반 클래스 참조로 잡은 예외가 어떻게 동작하는지 볼게요.
#include <iostream>
#include <vector>
int main()
{
try
{
std::cout << "Throwing an integer exception...\n";
throw 42;
}
catch (int i)
{
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
try
{
std::cout << "Creating a vector of size 5... \n";
std::vector<int> v(5);
std::cout << "Accessing the 11th element of the vector...\n";
std::cout << v.at(10); // vector::at()는 std::out_of_range를 던짐
}
catch (const std::exception& e) // 기반 클래스 참조로 잡음
{
std::cout << " a standard exception was caught, with message: '"
<< e.what() << "'\n";
}
}
가능한 출력:
Throwing an integer exception...
the integer exception was caught, with value: 42
Creating a vector of size 5...
Accessing the 11th element of the vector...
a standard exception was caught, with message: 'out_of_range'
결함 보고(Defect reports)
이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 98 | C++98 | switch 문이 핸들러로 제어를 옮길 수 있었음 |
금지됨 |
| CWG 210 | C++98 | throw 표현식이 핸들러와 매치됐음 |
예외 객체가 핸들러와 매치됨 |
| CWG 388 | C++98 | 포인터나 멤버에 대한 포인터 타입의 예외가 다른 타입의 const 참조로 매치될 수 없었음 | 변환 가능하면 매치되게 함 |
| CWG 1166 | C++98 | 타입이 추상 클래스 타입에 대한 참조인 핸들러가 매치될 때의 동작이 명시되지 않았음 | 추상 클래스 타입은 핸들러에 허용되지 않음 |
| CWG 1769 | C++98 | 핸들러 타입이 예외 객체 타입의 기반일 때 변환 생성자가 핸들러 매개변수 초기화에 쓰일 수 있었음 | 매개변수는 예외 객체의 대응 기반 클래스 부분 객체에서 복사 초기화됨 |
| CWG 2093 | C++98 | 객체에 대한 포인터 타입의 예외 객체가 한정 변환(qualification conversion)으로 객체에 대한 포인터 타입 핸들러와 매치될 수 없었음 | 허용됨 |
더 알아보기
try블록: 예외를 던질 수 있는 코드를 감싸는 블록.- 예외 던지기(Throwing exceptions):
throw표현식. - 예외 처리(Exception handling) 전반.