Program Language/C & C++

[C++] noexcept

야곰야곰+책벌레 2021. 10. 1. 15:11
728x90
반응형

C++11부터 throw()가 더 이상 사용되지 않고, noexcept 키워드가 추가되었다.

noexcept 키워드는 operator의 형태로, 그리고 specifier의 형태로 제공된다.

noexcept() 한정자는 모든 면에서 throw() 보다 강력하고, Stnadard library들을 사용함에 있어, noexcept 한정자는 성능 상의 추가 이득을 제공하기도 한다.

noexcept(expression)

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:
	~T() {}
	// 명시적 소멸.
	// 이동생성자/이동연산자 암시적으로 생성금지
	// 복사생성자/대인연산자는 noexcept로 암시적 생성
};

class U
{
	std::vector<int> v;
public:
	~U() {};
	// 명시적 소멸.
	// 이동생성자/이동연산자 암시적으로 생성금지
	// 복사생성자/대인연산자는 noexcept로 암시적 생성
	// vector<int> v의 복사생성자/대인연산자가 아니기 때문에..
};

class V
{
public:
	std::vector<int> v;
	// 이동생성자/이동연산자 noexcept(false)로 암시적 생성
	// 복사생성자/대인연산자는 noexcept로 암시적 생성
	// 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(lvalue u)는 복사생성자가 noexcept(false))
	noexcept(U(u)); // false (U(lvalue u)는 복사생성자가 noexcept(flase))
	noexcept(V(std::declval<V>())); // true (V(rvalue v)는 이동생성자가 noexcept)
	noexcept(V(v)); // false (V(lvaue v)는 복사생성자가 noexcept(false))

	return 0;
}

알듯 말듯 아리송한 예제다.

noexcept(expression) // 표현식은 상수 표현식이거나, bool로 변환가능해야 한다.
noexcept // noexcept(true)와 같다.

예제

#include <iostream>
#include <utility>
#include <vector>

template <typename T>
void foo() noexcept(noexcept(T())) {}

void bar() noexcept(true) {} // 예외를 던지지 않는다.
void baz() noexcept { throw 42; } // 예외를 던지지 않는다고 명시 후, 예외를 던짐.

int main()
{
	foo<int>();
	bar();
	baz(); // 컴파일 OK! , 런타임 에러 발생!!

	return 0;
}

함수에서 예외를 던지지 않겠다고 명시한 다음 예외를 던지면 std::terminated가 호출된다.

std::terminated가 호출되면 std::terminated_handler를 호출하는데, 기본이 std::abort이다.

프로그램은 바로 종료되어 버린다.

 

그리고 다음 함수들은 기본적으로 noexcept를 가진다.

  • 암시적으로 생성되는 기본 생성자, 복사 생성자, 대입 연산자, 이동 생성자, 이동 연산자, 소멸자
  • 유저가 명시적으로 noexcept(false)로 선언하거나, 부모의 소멸자가 그러하지 않은 경우를 제외한 모든 유저 정의 소멸자
  • operator delete 함수 시리즈 (할당 해제 함수들)
noexcept 연산자는 컴파일 타임에 평가되지만, noexcept 한정자는 컴파일 타임에 평가되지 않음을 주의.

 

noexcept 한정자가 붙은 함수에 대해서 컴파일러는 특정한 최적화를 진행할 수 있다.

noexcept를 쓰는 가장 큰 이유는 strong exception guarantee여야 한다.

C++11 이후 std::vector뿐 아니라 STL 컨테이너들은 move semantics가 모두 적용되어 있다.

원소에 대한 이동 처리를 할 때 해당 원소가 move시 noexcept를 지원하지 않으면,

move semantics가 아닌 copy semantics로 element를 처리한다.

 

즉, 이동처리에 대한 strong exception guarantee가 되어 있지 않으면 move semantics의 장점을 포기하는 것이다.

따라서, move semantics의 혜택을 제대로 누리려면, noexcept 한정자를 통해 strong exception guarantee를 보정해줘야 한다.

728x90
반응형

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

[C++] Callable  (0) 2021.10.06
[C++] condition_variable  (2) 2021.10.05
[C++] atomic  (6) 2021.10.01
[C++] explicit 키워드  (0) 2021.10.01
[C++] thread에서 return 값 받기 (promise/future)  (2) 2021.09.30