Item 14. 예외를 방출하지 않을 함수는 noexcept로 선언해라


C++98에서 사용하는 예외 명세의 한계

C++98에서는 사용하는 예외 명세(exception specification)는 비효율적이였다.

예외 명세(exception specification) : 함수가 내보낼 수 있는 예외 목록

개발자는 함수가 방출할 수 있는 예외 타입을 명세해야 했으며 함수의 구현을 수정하면 예외 명세도 바꾸어야 할 가능성이 있었다.

즉, 함수 구현과 예외 명세, 클라이언트 코드 사이의 일관성 유지에 아무런 도움을 주지 못했다.

// int, char 타입에 해당하는 예외만 발생하도록 명세
int func(int x) throw(int, char);

try {
  // 함수가 수정되면 예외 명세와 클라이언트 코드를 모두 바꿔야 할 수 있다
  int ret = func(input);
	
  // do something ..
} catch (char error) {
  std::cout << error << std::endl;
  
  // do someting ..
} catch (int error) {
  std::cout << std::error << std::endl;
  
  // do something ..
}

하지만 C++11부터 예외를 하나라도 방출할 수 있는 함수인지 여부만이 의미 있는 정보라고 판단하여 C++98에서 사용하던 예외 명세 방식은 비권장(deprecate) 기능으로 분류되었다.


noexcept

C++11부터는 noexcept 키워드를 통해서 예외를 방출할 수 있는 함수인지 아닌지 이분법적으로 나눌 수 있게 하였다.

함수 선언 뒤에 noexcept 키워드를 붙이면 해당 함수는 예외를 방출하지 않는다는 뜻이다.

또한 noexcept 키워드는 noexcept(exp) 형태로 표현할 수도 있다. (인자로 받은 expression이 예외를 방출할 수 있는지 없는지 판별한 후, 참(true)이면 예외를 방출할 수 없고 거짓(false)이면 방출할 수 있다.)

int func1() noexcept; // func1 함수는 예외를 방출하지 않음

int func2() noexcept(false); // func2 함수는 예외를 방출함

int func3() noexcept { throw 10; } // noexcept 키워드를 붙인 func3 함수에서는 throw를 써도 예외를 방출할 수 없음


noexcept 사용 시 장점

noexcept 키워드를 사용하면 다음과 같은 장점이 있다.

함수의 예외 방출 여부를 명확하게 알 수 있음

인터페이스를 설계할 때 함수에 noexcept 키워드를 사용할 것인지 아닌지를 결정하여야 한다.

사용자는 함수의 인터페이스를 통해 예외 방출 여부를 명확하게 알 수 있어서 좀 더 예외 안정성이나 효율성을 고려한 호출 코드를 구현할 수 있다.

컴파일러 최적화에 유용함

해당 함수가 예외를 방출하지 않는다는 선언을 다음과 같이 할 수 있다.

int func() throw();  // C++98 스타일, func 함수는 예외를 방출하지 않음

int func() noexcept; // C++11 스타일, func 함수는 예외를 방출하지 않음

이때 예외를 방출하지 않는다고 선언한 함수 func 내부에 예외를 방출하는 코드가 있다고 가정해보자.

C++98 스타일의 경우, 예외가 발생하면 호출 스택이 func 함수가 호출된 지점까지 풀린 후 (스택 되감기, stack unwinding) 프로그램이 종료(terminate)된다.

C++11 스타일의 경우, 예외가 발생하면 호출 스택이 func 함수가 호출된 지점까지 풀리지 않고 프로그램이 종료된다.

C++11 스타일은 스택 되감기를 할 필요가 없으므로 함수 호출 시점까지 스택을 풀 수 있는 상태 정보나 함수 내의 객체를 순서대로 파괴하기 위한 추가 코드를 작성할 필요가 없어서 추가적인 최적화가 가능하다.

ReturnType function(params) noexcept; // 최적화 여지가 가장 큼

ReturnType function(params) throw();  // 최적화 여지가 더 작음

ReturnType function(params);          // 최적화 여지가 더 작음

STL 최적화에 유용함

STL을 사용할 때도 noexcept 키워드는 중요한 역할을 한다.

대표적인 예시로 vector 컨테이너의 push_back 함수가 있다.

std::vector<Widget> vw;

Widget w;

// 만약 vw 컨테이너 메모리가 가득 찬 상태에서 삽입을 했으면?
vw.push_back(w);

std::vector<T> 컨테이너는 사용자가 직접 메모리를 할당하지 않아도 내부에서 일정한 크기의 메모리를 할당해서 사용하고 있다.

컨테이너의 원소가 많아져 메모리가 가득 찬 상태에서 삽입이 발생하면 내부에서 재할당이 일어난다.

이 과정에서 복사가 일어날 경우, 예외가 발생하더라도 예외 안정성을 보장할 수 있다. 복사 도중 예외가 발생하더라도 컨테이너의 기존 메모리에 있는 내용은 그대로기 때문이다.

하지만 이 과정에서 이동이 일어날 경우, 예외가 발생하면 예외 안정성을 보장할 수 없다. 이동 도중 예외가 발생하면 컨테이너의 기존 메모리에 있는 내용이 달라졌을 수도 있기 때문이다.

따라서 컴파일러는 이동 연산이 예외를 방출하지 않는다는 확신이 있어야만 이동 연산을 사용한다.

즉, T의 이동 생성자에 noexcept 키워드가 있어야 있어야 std::vector<T>는 재할당 과정에서 이동 연산을 수행한다.

std::vector::push_back 같은 함수들은 내부에서 std::move_if_noexcept 함수를 통해 이동 생성자가 noexcept 키워드를 가지고 있어야만 rvalue 형변환을 수행하는 식으로 상황에 맞게 복사/이동 연산을 수행한다.


다른 예시로 swap 함수가 있다.

swap 함수는 STL 알고리즘 구현의 핵심 구성요소로 복사 대입 연산자에서도 흔히 사용된다.

template <class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));

template <class T1, class T2>
struct pair {
  void swap(pair& p) noexcept(noexcept(noexcept(swap(first, p.first)) &&
                                       noexcept(swap(second, p.second)));
}

복잡해보이지만 결국 noexcept(expr)안의 expression이 참(noexcept)이어야 함수도 noexcept로 선언한다는 뜻이다.

이를 조건부 noexcept라 부른다.


기본적으로 noexcept로 선언되는 함수들

기본적으로 모든 메모리 해제 함수와 모든 소멸자 함수는 암묵적으로 noexcept이다. 즉, 직접 선언할 필요가 없다.

만약 이러한 함수에서 예외를 방출하고 싶으면 noexcept(false)를 사용하면 되지만 절대 권장하지 않는다.

이외에 다른 함수는 예외에 중립적(exception-neutral)이다. 예외에 중립적인 함수는 스스로 예외를 방출하지 않지만 예외를 방출하는 함수를 호출할 수 있다.


카테고리:

업데이트: