예외 사양
예외 사양 (Exception Specification)
함수가 직접 또는 간접적으로 던질 수 있는 예외의 종류를 목록으로 나열한 것이에요. 이전 C++에서 쓰인 throw(int) 같은 동적 예외 사양과, C++11의 noexcept가 대표적이에요.
출처: cppreference
본문
문법
throw(type-id-list (optional)) (1) (C++11에서 폐기, C++17에서 제거)
type-id-list— 쉼표로 구분된 type-id 목록. 팩 확장을 나타내는 type-id 뒤에는 생략 부호(...)가 옴 (C++11 이후).
- 명시적 동적 예외 사양입니다.
명시적 동적 예외 사양은, 함수 타입·함수에 대한 포인터 타입·함수에 대한 참조 타입·멤버 함수에 대한 포인터 타입을 위한 함수 선언자의 최상위 타입인 선언/정의에만, 또는 함수 선언자에서 매개변수/반환 타입으로 나타나는 그런 타입에만 올 수 있어요.
void f() throw(int); // OK: function declaration
void (*pf)() throw (int); // OK: pointer to function declaration
void g(void pfa() throw(int)); // OK: pointer to function parameter declaration
typedef int (*pf)() throw(int); // Error: typedef declaration
설명
함수가 자신의 동적 예외 사양에 타입 T가 나열된 채 선언되면, 그 함수는 그 타입 또는 그 타입에서 파생된 타입의 예외를 던질 수 있어요.
불완전 타입, 불완전 타입에 대한 포인터/참조(cv void* 제외), 그리고 rvalue 참조 타입(C++11 이후)은 예외 사양에 허용되지 않아요. 배열과 함수 타입을 쓰면 대응하는 포인터 타입으로 조정되고, 최상위 cv 한정도 떨어져요. 매개변수 팩은 허용돼요. (C++11 이후)
조정된 타입의 집합이 (팩 확장 후에)(C++11 이후) 비어 있는 동적 예외 사양은 **비-던지기(non-throwing)**예요. 비-던지기 동적 예외 사양을 가진 함수는 어떤 예외도 허용하지 않아요.
동적 예외 사양은 함수의 타입의 일부로 간주되지 않아요.
함수가 예외 사양에 나열되지 않은 타입의 예외를 던지면 std::unexpected 함수가 호출돼요. 기본 함수는 std::terminate를 호출하지만, 사용자 제공 함수( std::set_unexpected로)로 교체될 수 있고, 그 함수는 std::terminate를 호출하거나 예외를 던질 수 있어요. std::unexpected에서 던져진 예외가 예외 사양에 받아들여지면 스택 풀기(stack unwinding)가 평소처럼 계속돼요. 그렇지 않고 예외 사양이 std::bad_exception을 허용하면 std::bad_exception이 던져져요. 그 외에는 std::terminate가 호출돼요.
인스턴스화 (Instantiation)
함수 템플릿 특수화의 동적 예외 사양은 함수 선언과 함께 인스턴스화되지 않아요. 필요할 때만 인스턴스화돼요 (아래 정의).
암시적으로 선언된 특수 멤버 함수의 동적 예외 사양도 필요할 때만 평가돼요 (특히, 파생 클래스의 멤버 함수를 암시적으로 선언하는 것이 기본 클래스 멤버 함수의 예외 사양 인스턴스화를 요구하지 않아요).
함수 템플릿 특수화의 동적 예외 사양이 필요하지만 아직 인스턴스화되지 않았다면, 의존 이름이 조회되고 표현식에 쓰인 템플릿들이 특수화의 선언을 위해 인스턴스화된 것처럼 인스턴스화돼요.
함수의 동적 예외 사양은 다음 맥락에서 필요한 것으로 간주돼요:
- 표현식에서 오버로드 해석으로 함수가 선택되는 경우
- 함수가 odr-used인 경우
- 함수가 odr-used일 테지만 평가되지 않는 피연산자에 나타나는 경우
template<class T>
T f() throw(std::array<char, sizeof(T)>);
int main()
{
decltype(f<void>()) *p; // f unevaluated, but exception specification is needed
// error because instantiation of the exception specification
// calculates sizeof(void)
}
- 다른 함수 선언과 비교하기 위해 사양이 필요한 경우 (예: 가상 함수 오버라이더 또는 함수 템플릿의 명시적 특수화)
- 함수 정의에서
- 기본화된 특수 멤버 함수가 자신의 예외 사양을 결정하기 위해 검사해야 하므로 사양이 필요한 경우 (이것은 기본화된 특수 멤버 함수의 사양 자체가 필요할 때만 일어나요).
잠재적 예외 (Potential exceptions)
각 함수 f, 함수 포인터 pf, 멤버 함수 포인터 pmf는 잠재적 예외의 집합을 가져요. 이는 던져질 수 있는 타입들로 구성돼요. 모든 타입의 집합은 어떤 예외든 던질 수 있음을 나타내요. 이 집합은 다음과 같이 정의돼요:
f,pf,pmf의 선언이 모든 예외를 허용하지 않는 동적 예외 사양을 쓰면(C++11 이전), 집합은 그 사양에 나열된 타입들로 구성돼요.- (C++11 이후) 그렇지 않고 선언이
noexcept(true)를 쓰면, 집합은 비어 있어요. - 그렇지 않으면, 집합은 모든 타입의 집합이에요.
참고: 암시적으로 선언된 특수 멤버 함수(생성자, 할당 연산자, 소멸자)와 상속 생성자(C++11 이후)의 경우, 잠재적 예외 집합은 그들이 호출할 모든 것(비-variant 비정적 데이터 멤버/직접 기본/적절한 경우 가상 기본의 생성자·할당 연산자·소멸자 — 언제나처럼 기본 인자 표현식 포함)의 잠재적 예외 집합들의 조합이에요.
각 표현식 e는 잠재적 예외의 집합을 가져요. 집합은 e가 핵심 상수 표현식이면 비어 있고, 그렇지 않으면 e의 모든 직접 하위 표현식(기본 인자 표현식 포함)의 잠재적 예외 집합의 합집합에, e의 형태에 따라 달라지는 또 다른 집합을 더한 것이에요:
e가 함수 호출 표현식이면, 호출되는 함수/함수 포인터/멤버 함수 포인터를g라 할 때g의 선언이 동적 예외 사양을 쓰면,g의 잠재적 예외 집합이 더해져요.- (C++11 이후)
g의 선언이noexcept(true)를 쓰면, 집합은 비어 있어요. - 그렇지 않으면, 집합은 모든 타입의 집합이에요.
e가 함수를 암시적으로 호출하면(연산자 표현식인데 연산자가 오버로드된 경우,new표현식인데 할당 함수가 오버로드된 경우, 또는 전체 표현식인데 임시 객체의 소멸자가 호출된 경우), 집합은 그 함수의 집합이에요.e가 throw 표현식이면, 집합은 그 피연산자가 초기화할 예외이거나, 재-던지기 throw 표현식(피연산자 없음)의 경우 모든 타입의 집합이에요.e가 다형성 타입에 대한 참조로의dynamic_cast이면, 집합은std::bad_cast로 구성돼요.e가 다형성 타입에 대한 역참조된 포인터에 적용된typeid라면, 집합은std::bad_typeid로 구성돼요.- (C++11 이후)
e가 비상수 배열 크기의new표현식이고, 선택된 할당 함수의 잠재적 예외 집합이 비어 있지 않으면, 집합은std::bad_array_new_length로 구성돼요.
void f() throw(int); // f()'s set is "int"
void g(); // g()'s set is the set of all types
struct A { A(); }; // "new A"'s set is the set of all types
struct B { B() noexcept; }; // "B()"'s set is empty
struct D() { D() throw (double); }; // new D's set is the set of all types
모든 암시적으로 선언된 멤버 함수와 상속 생성자(C++11 이후)는 예외 사양을 가지며, 다음과 같이 선택돼요:
- 잠재적 예외 집합이 모든 타입의 집합이면, 암시적 예외 사양은 모든 예외를 허용해요 (예외 사양이 존재하는 것으로 간주되지만 코드로 표현할 수 없고, 예외 사양이 없는 것처럼 동작해요 — C++11 이전 /
noexcept(false)— C++11 이후). - 그렇지 않고 잠재적 예외 집합이 비어 있지 않으면, 암시적 예외 사양은 집합의 모든 타입을 나열해요.
- 그렇지 않으면, 암시적 예외 사양은
throw()(C++11 이전) /noexcept(true)(C++11 이후)예요.
struct A
{
A(int = (A(5), 0)) noexcept;
A(const A&) throw();
A(A&&) throw();
~A() throw(X);
};
struct B
{
B() throw();
B(const B&) = default; // exception specification is "noexcept(true)"
B(B&&, int = (throw Y(), 0)) noexcept;
~B() throw(Y);
};
int n = 7;
struct D : public A, public B
{
// May throw an exception of a type that would match a handler of type
// std::bad_array_new_length, but does not throw a bad allocation exception
(void*) new (std::nothrow) int[n];
// D may have the following implicitly-declared members:
// D::D() throw(X, std::bad_array_new_length);
// D::D(const D&) noexcept(true);
// D::D(D&&) throw(Y);
// D::~D() throw(X, Y);
};
Notes
Clang은 동적 예외 사양의 인스턴스화 규칙이 C++11에서 CWG1330으로 바뀌었다고 간주해요. LLVM #56349 참고.
키워드
throw
예제
여기서 확인할 것: 동적 예외 사양에 나열되지 않은 예외(W)를 던지면 std::unexpected가 호출되고, 등록된 handler가 실행되는지 보는 코드예요.
참고: 경고를 피하려면 C++98 모드로 컴파일하는 게 좋아요. C++17 이후 개정판과는 호환되지 않아요.
#include <cstdlib>
#include <exception>
#include <iostream>
class X {};
class Y {};
class Z : public X {};
class W {};
void f() throw(X, Y)
{
bool n = false;
if (n)
throw X(); // OK, would call std::terminate()
if (n)
throw Z(); // also OK
throw W(); // will call std::unexpected()
}
void handler()
{
std::cerr << "That was unexpected!\n"; // flush needed
std::abort();
}
int main()
{
std::set_unexpected(handler);
f();
}
출력:
That was unexpected!
주목할 점은 W는 사양(X, Y)에 없으므로 프로그램이 std::unexpected로 흐르고, 등록된 handler가 "That was unexpected!"를 출력한 뒤 std::abort()로 종료한다는 것이에요.
결함 보고 (Defect reports)
다음의 동작 변경 결함 보고는 이전에 발표된 C++ 표준에 소급 적용됐어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 25 | C++98 | 예외 사양이 다른 멤버 포인터 간 할당/초기화 동작이 미지정이었음 | 함수 포인터와 참조에 대한 제한을 적용 |
| CWG 973 | C++98 | 예외 사양이 함수 타입을 포함할 수 있었지만 대응하는 함수 포인터 변환이 명시되지 않았음 | 명시됨 |
| CWG 1330 | C++98 | 예외 사양이 조기에 인스턴스화될 수 있었음 | 필요할 때만 인스턴스화 |
| CWG 1267 | C++11 | rvalue 참조 타입이 예외 사양에 허용되었음 | 허용되지 않음 |
| CWG 1351 | C++98, C++11 | 기본 인자(C++98)와 기본 멤버 초기화자(C++11)가 암시적 예외 사양에서 무시되었음 | 고려됨 |
| CWG 1777 | C++11 | T가 빈 팩이어도 throw(T...)가 비-던지기 사양이 아니었음 |
팩이 비어 있으면 비-던지기 |
| CWG 2191 | C++98 | typeid 표현식의 잠재적 예외 집합이 던질 수 없어도 bad_typeid를 포함할 수 있었음 |
던질 수 있을 때만 포함 |
더 알아보기 (Learn more)
- noexcept 지정자 (C++11) — 함수가 예외를 던질 수 있는지 여부를 지정
- 예외 처리 — 예외 처리 전반