noexcept

Modern C++에 적응하기
Effective Modern C++ Chapter 3. noexcept


noexcept

C++11에서 함수가 예외를 방출(emit)[1]하지 않는다면, 함수를 noexcept로 선언할 수 있다.
noexcept는 함수 예외 안정성의 명시적 보장이다. 이를 통해서 클라이언트는 함수를 호출할때 해당 함수가 예외 안정성을 보장하는지 알 수 있다.


noexcept는 컴파일러가 코드를 최적화하는데 참고하는 고려사항 중 하나이다.
C++98에서 예외 안정성이 보장된 함수(int f(int x) throw();)에서 예외가 방출되면(즉, 예외 명세가 위반되면), 호출 스택이 해당 함수를 호출한 지점에 도착할 때 까지 풀리며(unwind), 그 지점에서 몇 가지 동작 후 프로그램이 종료된다.

반면 C++11에서 같은 예외 명세를 가진 함수(int f(int x) noexcept;)의 예외 명세가 위반되면, 프로그램이 종료되기 전에 호출 스택이 풀릴수도있고 아닐 수도 있다.[2]

만약 함수에서 예외가 방출된다고 해도, 그 함수가 noexcept라면 컴파일러는 호출 스택을 함수 호출 지점까지 풀릴 수 있도록 유지할 필요가 없다.
또한 예외가 함수를 벗어난다고 해도, 그 함수가 noexcept라면 함수 안의 객체들을 반드시 생성의 역순으로 파괴해야할 필요도 없다.
이러한 예외 유연성은 컴파일러가 코드를 최적화하는데 있어서 큰 도움을 줄 수 있다.
반면 C++98의 예외 명세(throw())는 위와같은 예외 유연성이 없다.

1
2
3
return-type func(params) noexcept; // 최적화 가능성이 가장 크다.
return-type func(params) throw(); // 최적화 가능성이 덜 크다.
return-type func(params); // 최적화 가능성이 가장 작다.

noexcept의 장점중 가장 대표적인 케이스는 이동(move) 연산이다.
std::vectorpush_back을 통해 새 요소를 추가할 때, reallocation이 발생할 수도 있다. 이 과정을 순서대로 나타내면 다음과 같다.

  1. 새로운 메모리 공간을 할당한다.
  2. 기존 요소들을 새로 할당한 메모리 공간에 복사한다.
  3. 기존 요소들을 파괴한다.

이러한 순서 때문에 push_back은 강한 예외 안정성을 보장한다. 즉, 요소를 복사하는 도중에 예외가 발생하더라도 원래의 상태는 유지된다.

C++11에 move-semantics가 도입되면서 요소를 복사하는 대신 이동하는 것으로 최적화 할 수 있다. 하지만 그렇게되면 push_back의 강력한 예외 안정성 보장이 위반될 수 있다. 요소를 이동하게되면 원래의 상태를 유지할 수 없고, 따라서 기존의 상태로 되돌릴 수 없기 때문이다.

그래서 C++11 컴파일러는 객체의 이동 연산이 예외를 방출하지 않을경우에만(noexcept) push_back 구현의 복사를 이동으로 최적화한다.[3]


예외를 방출하지 않는것이 명확한 함수는 noexcept로 선언하는 것이 당연하다. 하지만 최적화때문에 함수를 억지로 noexcept로 선언하려고 노력하는것은 다소 무리가 있다.

noexcept로 선언하는 것이 중요한 함수들 중 일부는 기본적으로 noexcept로 선언된다. 모든 메모리 해제함수와 모든 소멸자는 암묵적으로 noexcept이다. 따라서 이런 함수들은 noexcept를 명시할 필요가 없다.(해도 상관없지만 명시하지 않는것이 관례이다.)

단, 소멸자의 경우 예외를 방출한다면 이를 명시할 수 있다.(noexcept(false)) 이런 소멸자를 가진 객체를 멤버로 갖는 클래스의 소멸자는 암묵적으로 noexcept되지 않는다.
이런 소멸자는 흔치 않다. 만약 표준라이브러리가 사용하는 어떤 객체의 소멸자가 예외를 방출한다면, 정의되지 않은 행동(Undefined Behavior) 이 발생한다.



References


  1. 예외가 함수 바깥으로 전파되는 것을 의미. 예외 '발생’또는 '던지기(throw)'와는 구분되는 개념이다. ↩︎

  2. 그 여부에 상관없이 호출자의 나머지 코드(catch(...)를 포함해서)는 실행되지 않는다. noexcept가 보장하는 것이 호출자관점에서 본다면 예외가 방출되지 않은것(실제로는 예외가 방출 되었더라도)과 같기 때문이다. ↩︎

  3. std::vector::push_back 뿐만 아니라 표준 라이브러리의 여러 함수가 이런식으로 동작한다. ↩︎

Author

Joyus.Gim

Posted on

2022-07-26

Updated on

2022-07-26

Licensed under

Comments