throw(typeid, typeid, ...)
throw 한정자는 함수가 예외로 던질 수 있는 typeid의 목록을 인자로 받는다. typeid에 해당하는 타입이 클래스나 구조체라면, 상속받는 자식도 역시 예외를 던질 수 있는 타입으로 간주한다.
// 아무 타입도 지정하지 않았으므로, 예외를 던지지 않는다.
void no_except() throw();
// 모든 타입에 대해 예외를 던진다
void bar() throw(...) {}; // C++11부터 추가된 parameter pack, 즉 C++11 이전엔 이 형태가 불가능
void baz() {};
class X {};
class Y {};
class Z : public X {};
class W {};
// 함수 f는 X와 Y, 그리고 그들의 자식을 예외로 던질 수 있다.
void foo() throw(X, Y)
{
int n = 0;
if (n)
throw Y(); // OK
if (n)
throw Z(); // Z는 X의 자식이므로, OK
// 예외로 던질 수 없는 걸 던졌으므로, std::unexpected 호출
throw W();
}
std::unexpected가 호출되면 std::unexpected_handler를 호출하는데, 기본 handler 함수는 std::terminated이다. std::terminated가 호출되면 std::terminated_handler를 호출하는데, 기본 handler 함수는 std::abort이다. std::abort가 호출되면, 프로그램이 조용히 종료되어 버린다. unhandled exception이 발생하며 크래쉬가 발생하면, 덤프를 통해서 문제를 해결할 수 있는데 말이다. (물론, 이 문제는 noexcept도 마찬가지이다) 게다가, throw() 한정자는 다음의 문제들을 가지고 있다.
// 1. 예외 한정 확장의 경우
// 처음엔 하나의 예외만 던질 수 있었다가...
virtual void open() throw(FileNotFound);
// 다음과 같이 여러 개의 예외를 던질 수 있는 식으로 점점 확장될 수 있다.
// 만약 throw()에 포함되지 않는 타입이 예외를 던지게 되면, std::unexpected가 호출된다.
// 이는 원하지 않는 결과인 것이다.
virtual void open() throw(FileNotFound, SocketNotReady, InterprocessObjectNotImplemented, HardwareUnresponsive);
// 2. 예외 한정 불가의 경우
struct<typename T>
{
// T 타입을 생성시키는 함수
// T 타입에 따라 생성자가 예외를 던질수도 아닐수도 있다.
// 이 경우엔 throw()를 통해 문제를 해결할 수 없다.
void CreateOtherClass() { T t{}; }
};
이러한 throw의 문제점들이 인지되어, C++11부터 throw() 한정자는 deprecated 되었다. VS2013까지는 _NOEXCEPT가 throw()로 define되어 있으며, throw() 한정자가 deprecated되고, noexcept키워드가 반영되어 있지 않다.
2. noexcept
C++11(VS2015)부터 throw()가 deprecated 되고, noexcept 키워드가 추가되었다. noexcept 키워드는 연산자(operator)의 형태로, 그리고 한정자(specifier)의 형태로 제공된다. noexcept() 한정자는 모든 면에서 throw()보다 강력해졌고, 특히 C++11들의 Standard library들을 사용함에 있어, noexcept 한정자는 성능상의 추가 이득을 제공하기도 한다.
noexcept(expression)
noexcept 연산자는 컴파일 타임에 해당 표현식이 예외를 던지지 않는 표현식인지 체크하여, true/false를 반환한다.
표현식이 다음의 경우 중 하나라도 포함하고 있으면, noexcept는 false를 반환하며, 그렇지 않은 경우엔 true를 반환한다.
- 상수 표현식이 아닌 함수가 noexcept 키워드를 가지지 않을 경우
- 런타임 체크가 필요한 dynamic_cast 등의 RTTI가 포함된 경우
- typeid 표현식에 포함된 타입이 상속 관계에 있는 클래스나 구조체일 경우
#include <iostream>
#include <utility>
#include <vector>
// 예외를 던질 수 있음
void may_throw();
// 예외를 던지지 못함
void no_throw() noexcept;
// 예외를 던질 수 있는 람다
auto lmay_throw = [] {};
// 예외를 던지지 않는 람다
auto lno_throw = [] () noexcept {};
class T
{
public:
// 명시적인 소멸자 선언으로 인해 이동생성자/이동연산자 암시적 생성 금지
// 하지만 복사생성자/대인연산자는 noexcept로 암시적 생성
~T() {}
};
class U
{
std::vector<int> v;
public:
// 명시적인 소멸자 선언으로 인해 이동생성자/이동연산자 암시적 생성 금지
// 하지만, 복사생성자/대입연산자는 noexcept(false)로 암시적 생성
// vector<int> v의 복사생성자/대입연산자가 noexcept가 아니기 때문...
~U() {}
};
class V
{
public:
// 복사생성자/대입연산자는 noexcept(false)로 암시적 생성
// 이동생성자/이동연산자는 noexcept로 암시적 생성
// vector<int> v의 복사생성자/대입연산자가 noexcept가 아니기 때문...
std::vector<int> v;
};
int main()
{
T t;
U u;
V v;
noexcept(may_throw()); // false
noexcept(no_throw()); // true
noexcept(lmay_throw()); // false
noexcept(lno_throw()); // true
noexcept(std::declval<T>().~T()); // true (기본적으로 유저 정의 소멸자는 noexcept)
noexcept(T(std::declval<T>())); // true (T(rvalue t)는 이동생성자가 없으므로 복사생성자가 noexcept)
noexcept(T(t)); // true (T(lvalue t) 복사생성자가 noexcept)
noexcept(U(std::declval<U>())); // false (U(rvalue u)는 이동생성자가 없으므로 복사생성자가 noexcept(false))
noexcept(U(u)); // false (U(lvalue u)는 복사생성자가 noexcept(false))
noexcept(V(std::declval<V>())); // true (V(rvalue v)는 이동생성자가 noexcept)
noexcept(V(v)); // false (V(lvalue v)는 복사생성자가 noexcept(false))
}
// T() 생성자가 예외를 던지느냐에 따라, foo 함수의 예외 던질지 여부가 결정된다
// 기존 throw() 로는 이러한 처리가 불가능하다
// noexcept(T())는 operator로써, 이를 감싸는 외부 noexcept는 specifier로 사용되었다.
template <typename T>
void foo() noexcept(noexcept(T())) {}
// 예외를 던지지 않는다
void bar() noexcept(true) {}
// 예외를 던지지 않는다고 명시해놓고 예외를 던졌다.
void baz() noexcept { throw 42; }
int main()
{
// noexcept(noexcept(int())) => noexcept(true) OK
foo<int>();
bar(); // OK
baz(); // 컴파일은 문제 없으나, 런타임에 std::terminate가 호출된다.
}
함수에서 예외를 던지지 않겠다고 명시한 다음 예외를 던지면 std::terminated가 호출된다. std::terminated가 호출되면 std::terminated_handler를 호출하는데, 기본 handler 함수는 std::abort이다. std::abort가 호출되면, 프로그램이 조용히 종료되어 버린다.
이는 throw에서도 문제로 지적되었던 것인데 throw와의 차이점은 다음과 같다.
- throw는 std::unexpected를 호출하였고, noexcept는 std::terminated를 호출한다.
- throw와는 달리 noexcept는 stack unwinding이 발생할수도 아닐 수도 있어 컴파일러가 조금 더 최적화를 할 수 있게 한다.
그리고 다음의 함수들은 기본적으로 noexcept 를 가진다.
- 암시적으로 생성되는 기본 생성자, 복사 생성자, 대입 연산자, 이동 생성자, 이동 연산자, 소멸자
- 유저가 명시적으로 noexcept(false)로 선언하거나, 부모의 소멸자가 그러하지 않은 경우를 제외한 모든 유저 정의 소멸자
- operator delete 함수 씨리즈 (할당 해제 함수들)
이 중 암시적으로 생성되는 복사/이동 생성자나 대입/이동 연산자는 자신의 멤버들이 복사나 이동시 noexcept를 보장해야 이들의 생성자나 연산자가 기본적으로 noexcept를 가질 수 있다.
class T
{
public:
// 명시적인 소멸자 선언으로 인해 이동생성자/이동연산자 암시적 생성 금지
// 하지만 복사생성자/대인연산자는 noexcept로 암시적 생성
~T() {}
};
class U
{
std::vector<int> v;
public:
// 명시적인 소멸자 선언으로 인해 이동생성자/이동연산자 암시적 생성 금지
// 하지만, 복사생성자/대입연산자는 noexcept(false)로 암시적 생성
// vector<int> v의 복사생성자/대입연산자가 noexcept가 아니기 때문...
~U() {}
};
class V
{
public:
// 복사생성자/대입연산자는 noexcept(false)로 암시적 생성
// 이동생성자/이동연산자는 noexcept로 암시적 생성
// vector<int> v의 복사생성자/대입연산자가 noexcept가 아니기 때문...
std::vector<int> v;
};
noexcept 연산자는 컴파일 타임에 평가되지만, noexcept 한정자는 컴파일 타임에 평가되지 않음을 주의해야 한다. noexcept 한정자가 붙은 함수가 실제 예외를 던지게 되면, 런타임에 std::terminated가 호출된다는 것을 다시 한번 기억하기 바란다.
3. 언제 써야할까?
noexcept 한정자가 붙은 함수에 대해서 컴파일러는 특정한 최적화를 진행할 수 있다. 하지만 최적화 목적으로 사용은 비추한다. noexcept를 쓰는 가장 큰 이유는 strong exception guarantee여야 한다. 이것이 왜 필요하냐? C++11 이후 std::vector뿐 아니라 STL 컨테이너들은 move semantics가 모두 적용되어 있다. 원소에 대한 이동 처리를 할 때 해당 원소가 move시 noexcept를 지원하지 않으면, move semantics가 아닌 copy semantics로 element를 처리한다.즉, 이동 처리에 대한 strong exception guarantee가 되어 있지 않으면 move semantics의 장점을 포기하는 것이다.
#include <iostream>
#include <vector>
struct foo
{
int value;
explicit foo(int value) : value(value)
{
std::cout << "foo(" << value << ")\n";
}
foo(foo const& other) : value(other.value)
{
std::cout << "foo(foo(" << value << "))\n";
}
foo(foo&& other) noexcept : value(std::move(other.value))
{
other.value = -1;
std::cout << "foo(move(foo(" << value << "))\n";
}
~foo()
{
if (value != -1)
std::cout << "~foo(" << value << ")\n";
}
};
int main()
{
std::vector<foo> foos;
foos.emplace_back(1);
// 이때, 벡터의 확장을 위해 reserve가 발생.
// 만약 foo의 이동 생성자가 noexcept를 보장하지 않으면, copy construction이 발생한다
// 즉, reallocating 과정에서 이동이 아닌 복사가 발생하여 성능 이득을 얻지 못하게 되는 것이다
foos.emplace_back(2);
}
이는 vector의 reallocating이 발생하는 resize의 경우도 마찬가지이다. 이러한 처리는 noexcept가 도입된 C++11 컴파일러(VS2015)의 STL 컨테이너에 전반적으로 동일하게 적용된다.
4. move_if_noexept
STL 컨테이너들의 move semantics가 strong exception guarantee를 요구하는 것과 관련하여, C++11(VS2015) 라이브러리(utility.h)에 추가된 하나의 새로운 trait 템플릿을 소개한다.
template <typename T>
typename std::conditional<
!std::is_nothrow_move_constructible<T>::value && std::is_copy_constructible<T>::value,
const T&,
T&&
>::type move_if_noexcept(T& x);
이동(move) 또는 복사(copy)될 수 있는 "X" 를 인자로 받아, 이동 생성자가 noexcept이면 std::move(X)를 반환, 그렇지 않으면 X를 반환한다.
#include <iostream>
#include <utility>
struct Good
{
Good() {}
Good(Good&&) noexcept // 예외를 던지지 않는다
{
std::cout << "Non-throwing move constructor called\n";
}
Good(const Good&) noexcept // 예외를 던지지 않는다
{
std::cout << "Non-throwing copy constructor called\n";
}
};
struct Bad
{
Bad() {}
Bad(Bad&&) // 예외를 던질 수 있다
{
std::cout << "Throwing move constructor called\n";
}
Bad(const Bad&) // 예외를 던질 수 있다
{
std::cout << "Throwing copy constructor called\n";
}
};
int main()
{
Good g;
Bad b;
// Good 이동 생성자는 예외를 던지지 않으므로, std::move(g)를 반환
// 이동 생성자가 실행된다
Good g2 = std::move_if_noexcept(g);
// Bad 이동 생성자는 예외를 던질 수 있으므로, b를 반환
// 복사 생성자가 실행된다
Bad b2 = std::move_if_noexcept(b);
}
출처 : http://sweeper.egloos.com/3148916#comment_3148916
'프로그래밍 언어 > C++' 카테고리의 다른 글
C++ 구조적 바인딩 (Structured Bindings) (0) | 2022.06.21 |
---|---|
C++ STL 설명과 for-range 기반 loop (0) | 2022.06.21 |
C++ 전방 선언 (Forward Declaration) (0) | 2022.06.16 |
C++ typename의 두 가지 의미 (0) | 2022.06.16 |
C++ r-value && (임시 객체) / l-value & (고유 객체, 주소값) (0) | 2022.06.15 |