본문 바로가기

컴퓨터과학

[Effective C++] (14-3) 예외를 방출하지 않을 함수는 noexcept로 선언할 것

C++ noexcept

noexcept가 특히나 바람직한 또 다른 예로 swap 함수들이 있습니다. swap은 여러 STL 알고리즘 구현에서 핵심 구성 요소이며, 복사 배정 연산자들에서도 흔히 쓰입니다. 그처럼 여러 곳에서 쓰이기 때문에, noexcept를 통해서 최적화할 가치가 큽니다. 흥미롭게도, 표준 라이브러리에 있는 swap들의 noexcept 여부는 사용자 정의 swap들의 noexcept 여부에 어느 정도 의존합니다. 예를 들어 다음은 표준 라이브러리에 있는 배열에 대한 swap과 std::pair에 대한 swap의 선언들입니다.

 

template
void swap(T (&a)[N],
T (&b)[N]) noexcept(noexcept(swap(*a, *b))); // 본문 참고

template
struct pair {
...
void swap(pair& p) noexcept(noexcept(swap(first, p.first)) &&
noexcept(swap(second, p.second)));
...
};

 

이 함수들은 조건부 noexcept입니다. 즉, 이들이 noexcept인지의 여부는 noexcept절 안의 표현식들이 noexcept인지에 의존합니다. 예를 들어 Widget 배열이 두 개 있을 때, 그 둘을 교환하는 swap은 오직 배열의 개별 요소들의 swap이 noexcept일 때에만 noexcept입니다. 즉, Widget 배열들에 대한 swap은 Widget들에 대한 swap이 noexcept일 때에만 noexcept인 것입니다. 따라서, Widget 배열들에 대한 swap이 noexcept인지는 Widget을 위한 swap을 작성한 프로그래머가 결정합니다.

 

Widget 배열들에 대한 swap의 noexcept 여부는 다른 swap들, 이를테면 Widget 배열들의 배열들에 대한 swap의 noexcept 여부를 결정합니다. 마찬가지로, Widget들을 담은 두 std::pair 객체의 swap이 noexcept인지는 Widget들에 대한 swap의 noexcept 여부에 의존합니다. 더 높은 수준의 자료구조들의 교환이 일반적으로 noexcept인지의 여부가 오직 더 낮은 수준의 구성요소들의 교환이 noexcept인지의 여부에 의존한다는 사실은, swap 함수를 작성할 때에는 가능한 한 항상 noexcept를 지정하는 것이 바람직하다는 좋은 이유가 됩니다.

 

이제 독자도 noexcept가 제공하는 최적화 기회들에 꽤나 고무되었을 것입니다. 그러나, 안됐지만 흥분을 잠시 가라앉힐 필요가 있습니다. 최적화가 중요하긴 하지만, 더 중요한 것은 정확성(correctness)입니다. 이번 항목의 도입부에서 말했듯이, noexcept는 함수의 인터페이스의 일부입니다. 따라서 함수의 구현이 예외를 방출하지 않는다는 성질을 오랫동안 유지할 결심이 선 경우에만 함수를 noexcept로 선언해야 합니다. 만일 함수를 noexcept로 선언하고는 나중에 마음을 바꾼다면, 딱히 흡족한 수습 방안이 없습니다. 함수의 선언에서 noexcept를 제거(즉, 함수의 인터페이스를 변경)할 수는 있지만, 그러면 클라이언트 코드가 깨질 위험이 생깁니다. 예외를 방출할 수 있도록 함수의 구현을 변경하되 함수의 예외 명세(이제는 정확하지 않은)를 그대로 둘 수도 있지만, 그러면 예외가 실제로 함수를 벗어났을 때 프로그램이 종료됩니다. 아니면, 기존 구현의 책임자 자리에서 물러나서, 구현을 바꾸려는 시도 자체를 포기할 수도 있습니다. 어떤 방법이든 별로 마음에 들지 않을 것입니다.

 

중요한 것은, 대부분의 함수가 예외에 중립적(exception-neutral)이라는 점입니다. 예외 중립적 함수는 스스로 예외를 던지지는 않지만, 예외를 던지는 다른 함수들을 호출할 수는 있습니다. 다른 함수가 예외를 던지면 예외 중립적 함수는 그 예외를 그대로 통과시킵니다(호출 사슬의 위쪽 어딘가에 있는 예외 처리부에서 처리하길 바라면서). 이처럼 "그냥 통과하는" 예외가 존재할 수 있으므로, 예외 중립적 함수는 결코 noexcept가 될 수 없습니다. 따라서 대부분의 함수에 noexcept가 지정되어 있지 않은 것은 당연한 일입니다.

 

그러나, 예외를 전혀 방출하지 않는 것이 자연스러운 구현인 함수들도 있으며, noexcept로 선언하면 최적화에 큰 도움이 되는 함수들도 많습니다(특히 이동 연산들과 swap). 그,런 함수들을 가능하면 noexcept로 구현하는 것은 가치 있는 일입니다. 독자의 어떤 함수가 예외를 방출하지 않는다는 점을 확신할 수 있다면, 당연히 noexcept로 선언해야 합니다.

 

(* 표준 라이브러리에 있는 컨테이너들에 대한 이동 연산들의 인터페이스 명세에는 noexcept가 빠져 있습니다. 그러나 표준은 C++구현이 펴준 라이브러리 함수들에 대한 예외 명세를 더 강하게 변경하는 것을 허용합니다. 실제로, 적어도 몇몇 컨테이너의 경우에는 이동 연산들을 noexcept로 선언하는 컴파일러가 많습니다. 그러한 관행은 이 항목의 조언이 바람직함을 보여주는 좋은 예입니다. 표준 라이브러리 구현자들은 컨테이너 이동 연산이 예외를 던지지 않아도 된다는 점을 알게 되면, 비록 표준이 요구하지 않더라도, 그런 연산을 noexcept로 선언하는 경우가 많습니다.)