스토리텔링 개발자

[Effective Modern C++] 14. noexcept 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 14. noexcept

김디트 2025. 2. 24. 12:00
728x90

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

 

 

 

C++98의 예외 명세
  • 함수의 구현을 수정하면 예외 명세도 바뀌는 상황이 잦았다.
    • 클라이언트 코드가 깨질 수 있다.
    • 호출자가 원래의 예외 명세에 의존할 수도 있기 때문이다.
  • 컴파일러는 함수 구현과 예외 명세, 클라이언트 코드 사이의 일관성 유지에 아무런 도움도 안 됐다.
  • 결국 C++98의 예외 명세는 득보다 실이 크게 느껴졌다.

 

 

C++11의 예외 명세
  • 자세한 예외 지정보다는 예외를 하나라도 방출하는지 여부가 더 중요한 게 아닌가?
  • noexcept 키워드를 사용할 수 있게 되었다.
  • C++98의 예외 지정은 비권장(deprecate) 기능으로 분류되었다.

 

 

noexcept의 장점
  • 호출 코드의 예외 안정성이나 효율성을 증진시킬 수 있다.
  • 컴파일러가 더 나은 목적 코드(object code)를 산출할 수 있다.
int f(int x) throw(); // c++98 방식의 예외 던지지 않음 표시
// 실행 시점에서 예외가 f 바깥으로 던져지면 예외 명세 위반
// 1. 호출 스택이 f를 호출한 시점까지 rewind된다.
// 2. 실행 종료.

int f(int x) noexcept; // c++11 방식의 예외 던지지 않음 표시
// 실행 시점에서 예외가 f 바깥으로 던져지면 예외 명세 위반
// 1. 호출 스택이 f를 호출한 시점까지 rewind 될 수도 있고 아닐 수도 있다.
// 2. 실행 종료.
  • 호출 스택이 rewind 안되어도 된다는 점은, 효율적인 컴파일러 코드 작성에 큰 도움이 된다.
  • std::vector::push_back 같은 표준 라이브러리의 여러 함수는 '가능하면 이동하되, 필요하면 복사한다' 전략을 활용한다.
    • 그 이유는 예외 안정성 때문. 
    • 복사 방식의 push_back
      • 기존 메모리에서 새 메모리로 일일이 복사한 후
      • 기존 메모리의 객체들을 파괴한다.
      • 기존 메모리 객체들이 마지막에 파괴되므로 강력한 예외 안정성
    • 이동 방식의 push_back
      • 기본 메모리에서 새 메모리로 일일이 이동한다.
      • 이동시마다 기존 메모리가 변경되므로 예외 안정성에 문제가 있을 수 있다.
      • 헌데 noexcept 선언된 연산이면 이동 방식을 우선시 할 수 있을 것이다.
  • swap은 여러 알고리즘에서 사용되므로 noexcept로 최적화되면 좋을 것이다.
  • 아래는 표준 라이브러리에서의 배열에 대한 swap과 std::pair의 선언들이다.
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(swap(first, p.first)) &&
                                noexcept(swap(second, p.second)));
    ...
};
  • 이 함수들은 조건부 except이다.
    • 이들이 noexcept인지 여부는 noexcept절 안의 표현식들이 noexcept인지에 의존한다.
    • 예컨대 Widget 배열이 두 개 있다면
      • 그 둘을 교환하는 swap은 오직 배열의 개별 요소들의 swap이 noexcept일때만 noexcept이다.
    • 그러므로 noexcept swap을 사용할지 말지는 swap을 작성하는 프로그래머의 몫이다.
  • 즉, swap 함수를 작성할 때는 가능한 한 항상 noexcept를 지정하는 것이 좋다.

 

 

 

noexcept 선언 시 조심할 것
  • noexcept 선언 후 나중에 마음을 바꾼다 해도 흡족한 수습 방안이 없다.
    • noexcept를 제거하면, 사용한 쪽의 코드가 깨질 위험이 생긴다.
    • noexcept를 유지하고 그냥 예외를 던지면, 예외가 실제 발생했을 때 크래시가 발생할 것이다.
  • 대부분의 함수는 예외 중립적이다.
    • 예외를 발생시키진 않지만, 내부 다른 함수에서 던져진 함수를 그냥 통과시킬 순 있다.
    • 이 경우도 noexcept를 지정할 수 없다.
  • 즉, noexcept가 자연스러울 때만 지정하도록 하자.
    • 작위적으로 비틀어서 억지로 noexcept 지정하는 건 절대 잘될 리 없다.

 

 

 

암묵적으로 noexcept로 선언되는 함수
  • 메모리 해제 함수(operator delete, operatr delete[])
  • 모든 소멸자
    • 명시적으로 예외를 던질 수 있다고 지정할 수는 있다.

 

 

 

넓은 계약(wide contract)과 좁은 계약(narrow contract)
  • 넓은 계약
    • 전제조건이 없는 함수.
    • 프로그램 상태와 무관하게 호출할 수 있다.
    • 호출자가 전달하는 인수들에 그 어떤 제약도 가하지 않는다.
    • 결코 미정의 행동을 보이지 않는다.
    • 이 경우 noexcept로 선언하는 건 쉬운 일이다.
  • 좁은 계약
    • 넓은 계약이 아닌 모든 함수.
    • 전제조건을 위반하는 상황이 생길 수 있으므로 noexcept 선언하는 건 까다롭다.
      • 위반 시 예외를 던지지 못하게 될 것이다.

 

 

 

함수 구현과 예외 명세 사이의 비일관성을 파악하는 데 컴파일러는 도움이 안된다.
void setup();
void cleanup();

void doWork() noexcept
{
    setup();
    ...
    cleanup();
}
  • setup과 cleanup을 호출함에도 noexcept로 선언되어 있다?
    • setup과 cleanup이 비록 noexcept로 선언되어 있지 않더라도 예외를 던지지 않는 함수일 수 있다.
  • 그렇기 때문에 다음과 같은 코드를 컴파일러는 허용한다.(경고조차 없다.)
728x90
Comments