본문 바로가기
C++/Effective Modern C++

예외를 방출하지 않을 함수는 noexpect로

by COCO1337 2020. 7. 7.

함수를 noexcept로 선언할 것인지 여부는 인터페이스 설계상 문제이다. 즉, 함수 호출자는 noexcept 여부에 의존할 수 있다는 것을 뜻한다. 예외를 방출하지 않음이 확실한 함수를 선언할 때 noexcept를 사용하지 않는 것은 인터페이스 명세가 허술함을 의미한다.

예외를 받게 되는 일이 없음을 약속하기 위한 표현법이다.

int f(int x) throw();	// C++98 버전
int f(int x) noexcept;	// C++11 버전

실행 시점에서 예외가 f 바깥으로 나가게 되면 f의 에외 명세에 위반된다. C++98에서는 예외 명세가 위반되면 호출 스택이 f를 호출한 시점에 도달할 때까지 풀리며(unwind), 그 지점에서 몇 가지 동작이 취해진 후 종료된다(terminate). C++11에서는 프로그램 실행이 종료되기 전에 호출 스택이 풀릴 수도 있고 풀리지 않을 수도 있다.

throw() 함수는 호출 스택이 unwind 되는 과정에서 생성의 반대 순서로 파괴되지만 noexpect 함수에서는 실행시점 스택을 풀기 가능한 상태로 만들 필요가 없다. 즉 최적화 여지가 가장 크다는 것이다.(?)


이동 연산들에 대해 noexcept는 상당히 강력하다.

std::vector에서 새 요소를 추가할 때, std::vector에 충분한 공간이 없을 수도 있다. 그런 일이 생기면 std::vector는 자신의 요소들을 담을 더 큰 메모리 공간을 할당하고, 기존의 메모리 조각 요소들을 새 조각으로 옮긴다. 기존 메모리의 모든 요소가 새 메모리에 성공적으로 복사되기 전에는 기존 메모리의 그 어떤요소도 파괴되지 않기 때문에 push_back은 강한 예외 안전성을 보장할 수 있었다.

C++11 에서는 요소들의 복사를 이동으로 대체함으로써 요소 옮기기를 최적화 하는 것이 자연스럽지만, 그렇게 되면 예외 안전성이 위반될 수 있다. 이동 중에 예외가 발생하면 push_back 연산이 완료 되지 못하고 실패하게 되는데 이 때, std::vector는 이미 수정된 상태이고 복원이 불가능 할 수 있다.

때문에 표준 라이브러리의 표준 함수들은 "가능하면 이동하되 필요하면 복사한다" 전략을 사용한다.

이러한 예로 swap 함수가 있다.

// std::pari에 대한 swap의 선언
template<class T, size_t N>
void swap(T (&a) [N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));

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

이 함수들은 조건부 noexcept로, 이들이 noexcept 인지 여부는 noexcept 절 안의 표현식들이 noexcept인지에 의존한다.

더 높은 수준의 자료구조들의 교환이 일반적으로 noexcept인지 여부가 오직 더 낮은 수준의 구성요소들의 교환이 noexcept인지 여부에 의존한다는 사실은 swap 함수를 작성할 때에는 가능한 항상 noexcept를 지정하는 것이 바람직하다는 것이 좋은 이유가 된다.


대부분 함수들은 예외에 중립적이다. 예외 중립적 함수는 스스로 예외를 던지지는 않지만 예외를 던지는 다른 함수들을 호출할 수 있다. 하지만 예외를 전혀 방출하지 않는 것이 자연스러운 구현인 함수들도 있으며, noexcept로 선언하면 최적화에 큰 도움이 되는 함수들도 많다.

함수의 직접적인 구현이 예외를 던질 수 있다고 할 때, 이를 숨기기 위해 억지로 고치면 함수의 구현이 복잡해지고 호출 지점의 코드도 복잡해질 가능성이 크다.

마지막으로, 함수 구현과 예외 명세 사이의 비일관성을 파악하는데 컴파일러는 큰 도움을 주지 않는다.

void setup();
void cleanup();

void doWork() noexcept
{
    setup();
    ....
    cleanup();
}

이런 코드가 있다고 했을 때, doWork가 noexcept 함수가 아닌 setup(), cleanup() 함수를 호출함에도 noexcept로 선언되어 있다. 이렇게 noexcept 보장이 없는 코드에 의존하는 경우가 있지만 일반적으로 컴파일러는 경고 메시지를 표시하지 않는다.


Conclusion

- noexcept는 함수의 인터페이스의 일부이다. 이는 호출자가 noexcept 여부에 의존할 수 있음을 뜻한다.

- noexcept 함수는 비noexcept 함수보다 최적화의 여지가 크다

- noexcpet 는 이동 연산들과 swap, 메모리 해제 함수들, 그리고 소멸자들에 특히나 유용하다.

- 대부분의 함수는 noexcept가 아니라 예외에 중립적이다.


Reference

Effective Modern C++ 항목 14: 예외를 방출하지 않을 함수는 noexcept로 선언하라.

반응형

댓글