[Modern C++] 항목 14: 예외를 방출하지 않을 함수는 noexcept로 선언하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 3. 현대적 C++에 적응하기
👉🏻 항목 14: 예외를 방출하지 않을 함수는 noexcept로 선언하라
🔍 C++98 vs C++11 예외 명세
- C++98 방식: 함수가 방출할 수 있는 예외 타입들을 명시해야 했다.
함수 구현이 바뀌면 예외 명세도 바꿔야 했고, 이는 클라이언트 코드를 깨뜨릴 수 있었다. - C++11 방식: 진짜 중요한 정보는 “예외를 하나라도 방출하는가, 아니면 절대로 방출하지 않는가”라는
이분법적 정보뿐이라는 공감대가 형성되었다.
int f(int x) throw(); // f는 예외를 방출하지 않음: C++98 방식
int f(int x) noexcept; // f는 예외를 방출하지 않음: C++11 방식
noexcept는 함수 인터페이스의 일부다.- 호출자는
noexcept여부에 의존할 수 있으며, 이는 멤버 함수의const여부만큼이나 중요한 정보다.
⚡ noexcept의 최적화 이점
noexcept로 선언된 함수는 컴파일러가 더 나은 목적 코드(object code)를 생성할 수 있다.
반환타입 함수이름(매개변수목록) noexcept; // 최적화 여지가 가장 크다
반환타입 함수이름(매개변수목록) throw(); // 최적화 여지가 더 작다
반환타입 함수이름(매개변수목록); // 최적화 여지가 더 작다
- C++98 방식(
throw()): 예외 명세가 위반되면 호출 스택이 f를 호출한 지점까지 풀리며(unwind), 프로그램 실행이 종료된다. - C++11 방식(
noexcept): 프로그램 실행이 종료되기 전에 호출 스택이 풀릴 수도 있고 풀리지 않을 수도 있다. - 컴파일러 최적화기는
noexcept함수에서 런타임 스택을 풀기 가능 상태로 유지할 필요가 없으므로, 더 자유롭게 최적화할 수 있다.
🔄 이동 연산과 noexcept
noexcept가 특히 중요한 대표적인 예는 이동 연산이다.
std::vector<Widget> vw;
Widget w;
...
vw.push_back(w); // w를 vw에 추가
std::vector에 새 요소를 추가할 때 공간이 부족하면, 더 큰 메모리를 할당하고 기존 요소들을 옮긴다.- C++98: 복사 연산으로 요소를 옮겼다. 복사 도중 예외가 발생해도 원본은 그대로이므로 강한 예외 안전성 보장 가능.
- C++11: 복사 대신 이동으로 최적화하고 싶지만, 이동 도중 예외가 발생하면 원본이 이미 수정된 상태이므로 복원이 불가능할 수 있다.
따라서 std::vector::push_back은, “가능하면 이동하되 필요하면 복사한다” 전략을 사용한다.
- 이동 연산이
noexcept로 선언된 경우에만 복사를 이동으로 대체한다. - 이동 연산을
noexcept로 선언하면 성능 향상에 직접적으로 기여한다.
🔀 swap과 noexcept
swap도 noexcept가 특히 유용한 함수다.
표준 라이브러리의 swap의 noexcept 여부는, 사용자 정의 swap의 noexcept 여부에 의존한다.
// 배열에 대한 swap
template <class T, size_t N>
void swap(T (&a)[N], T (&b)[N])
noexcept(noexcept(swap(*a, *b))); // 조건부 noexcept
// std::pair에 대한 swap
template <class T1, class T2>
struct pair {
...
void swap(pair& p)
noexcept(noexcept(swap(first, p.first)) &&
noexcept(swap(second, p.second))
); // 조건부 noexcept
};
- 이 함수들은 조건부
noexcept다.noexcept절 안의 표현식들이noexcept인지에 따라 결정된다.
- 상위 수준 자료구조의
swap이noexcept인지는, 하위 구성요소들의swap이noexcept인지에 달려 있다.
→ 따라서 swap 함수를 작성할 때는 가능한 한 항상 noexcept를 지정하는 것이 바람직하다.
⚠️ noexcept 선언 시 주의사항
noexcept는 함수의 인터페이스의 일부이다.- 따라서, 함수가 예외를 방출하지 않는다는 성질을 오랫동안 유지할 결심이 선 경우에만 선언해야 한다.
- 나중에
noexcept를 제거하면 클라이언트 코드가 깨질 위험이 있다. - 대부분의 함수는 예외에 중립적(exception-neutral)이다.
- 스스로 예외를 던지지는 않지만, 예외를 던지는 다른 함수들을 호출할 수 있다.
- 따라서, 대부분의 함수에
noexcept가 지정되어 있지 않은 것은 당연하다.
- 구현을 억지로 비틀어
noexcept를 선언하는 것은 좋지 않다.- 구현이 복잡해지고 호출 지점 코드도 복잡해질 수 있다.
// noexcept가 자연스러운 구현인 경우에만 선언
void setup(); // 다른 어딘가에 정의된 함수들
void cleanup();
void doWork() noexcept
{
setup(); // 필요한 준비 작업을 수행
...
cleanup(); // 정리 작업을 수행
}
doWork는 비noexcept함수인setup과cleanup을 호출하지만noexcept로 선언되어 있다.
이는setup과cleanup이 실제로 예외를 던지지 않는다는 것을 알고 있는 경우 적법하다.
C++은 이런 코드를 허용하며, 일반적으로 컴파일러는 경고를 표시하지 않는다.
🔒 기본적으로 noexcept인 함수들
다음 함수들은 기본적으로 noexcept로 선언된다.
- 모든 메모리 해제 함수 (
operator delete,operator delete[]) - 모든 소멸자 (사용자 정의 소멸자이든, 컴파일러가 자동으로 작성하는 것이든)
- 소멸자가 암묵적으로
noexcept로 선언되지 않는 유일한 경우:noexcept(false)로 선언된 소멸자를 가진 타입의 멤버가 클래스에 있을 때
- 소멸자가 암묵적으로
📋 넓은 계약 vs 좁은 계약
라이브러리 설계자들은 넓은 계약(wide contract)과 좁은 계약(narrow contract) 함수를 구분한다.
- 넓은 계약 함수
- 전제조건이 없는 함수.
- 프로그램 상태와 무관하게 호출 가능하며, 미정의 행동을 보이지 않는다.
- 예외를 던지지 않음이 확실하다면
noexcept로 선언하기 쉽다.
- 좁은 계약 함수
- 전제조건이 있는 함수
- 전제조건이 위반되면 결과는 미정의 행동이다.
- 전제조건 위반을 예외로 알리고 싶어도,
noexcept로 선언하면 예외를 던질 수 없으므로 선언이 까다롭다. - 이 때문에 좁은 계약을 가진 함수에 대해서는
noexcept사용을 꺼리는 경향이 있다.
🧐 정리
noexcept는 함수 인터페이스의 일부다.
호출자가noexcept여부에 의존할 수 있음을 의미한다.noexcept함수는 비noexcept함수보다 최적화 여지가 크다.noexcept는 이동 연산,swap, 메모리 해제 함수, 소멸자에 특히 유용하다.- 대부분의 함수는
noexcept가 아니라 예외에 중립적이다.
댓글남기기