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 | return-type func(params) noexcept; // 최적화 가능성이 가장 크다. |
noexcept
의 장점중 가장 대표적인 케이스는 이동(move) 연산이다.
std::vector
에 push_back
을 통해 새 요소를 추가할 때, reallocation이 발생할 수도 있다. 이 과정을 순서대로 나타내면 다음과 같다.
- 새로운 메모리 공간을 할당한다.
- 기존 요소들을 새로 할당한 메모리 공간에 복사한다.
- 기존 요소들을 파괴한다.
이러한 순서 때문에 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) 이 발생한다.