Item14는 '예외를 방출하지 않을 함수는 noexcept 로 선언하라.' 라는 주제이다.
noexcept 를 사용해야하는 이유와 사용하면 좋은 장점들에 대해 설명하고 있다.
---
1. 예외 명세의 변화.
C++98의 예외 명세(예: void f() throw(int, MyError);)는 매우 까다로웠다. 함수가 던질 수 있는 모든 예외 타입을 일일이 나열해야 했고, 함수 구현이 바뀌면 명세도 수정해야 했다. 결국 관리가 어려워 C++98에서의 예외 명세는 개발자들에게 외면을 받았다.
하지만 C++11에선 예외 명세를 "예외를 던지는가, 아니면 절대로 던지지 않는가?"라는 이분법적 사고로 단순화 했다.
-> noexcept : 이 함수는 절대로 예외를 밖으로 던지지 않을 것을 보장한다는 강력한 의미이다.
---
2, 인터페이스 설계 관점의 noexcept
함수가 noexcept인지 아닌지는 사용자가 함수를 호출할 때 고려해야 하는 중요한 정보이다. const의 여부만큼 중요하다고 책에선 강조하고 있다. 예외를 던지지 않는다는 사실을 알고도 noexcept를 선언하지 않는 것은 인터페이스 설계에 소홀히 한 것과 다름없다.
---
3. 컴파일러 최적화
noexcept를 붙여야 하는 또다른 이유는 성능 때문이다.
- 일반 함수 : 예외가 발생하면 스택을 끝까지 되감기(unwinding) 한 후 프로그램을 종료한다. 또한 예외가 발생할 수 있다는 의미이기에 컴파일러는 예외가 터질때를 대비해 스택에 생성된 객체들을 역순으로 파괴하며 거슬러 올라가는 "스택 되감기"용 전처리 코드를 만들어야 한다.
- noexcept 함수 : 예외가 발생하면 스택을 되감지 않을 수도 있다. 컴파일러는 예외가 나가지 않는다고 확신하므로, 위와 같은 부가적인 코드들을 만들 필요가 없다.
RetType function(params) noexcept; // 가장 최적화 됨(most optimizable)
RetType function(params) throw(); // 덜 최적화 됨(less optimizable)
RetType function(params); // 덜 최적화 됨(less optimizable)
사고 발생 시
- 만약 noexcept 함수에서 예외가 밖으로 나가려 하면, std::terminate를 호출해 프로그램을 즉시 종료시킨다. 이 덕분에 실행 파일의 크기가 줄어들고 코드 속도가 빨라진다.
| 선언 방식 | 최적화 수준 | 특징 |
| noexcept | 최고 (most) | 스택 되감기 코드 생략, 즉시 종료 보장 |
| throw() | 낮음 (Less) | C++98 방식, 런타임 체크 오버헤드 있음 |
| (미선언) | 낮음 (Less) | 예외 가능성을 염두에 둔 보수적 코드 생성 |
---
4. Move if noexcept : std::vector의 이사 전략
핵심이다. 우리가 만드는 클래스의 이동생성자에 noexcept를 붙여야 하는 이유에 대해 설명한다.
- C++98(복사 이사) : 강력한 예외 안정성(String Exception Safety)을 위해 기존 요소를 새 공간으로 복사한다. 이동 생성자를 통한 복사 중 예외가 나도 원본이 남아있기 때문이다.
- C++11(이동 이사) : 이동 생성자에서 복사 대신 이동(Move)을 한다. 속도는 빠르지만 이동중 예외가 발생하면 원본 데이터가 손상되어 복구가 불가능해진다.
- Vector는 이동중에 예외가 발생하면 원본을 복구할 수 없기 때문에 기본적으로 복사를 택한다.
- noexcept를 사용해서 명시적으로 예외가 호출되지 않음을 선언하면 vector는 복사가 아닌 이동을 택한다.
아래 코드를 보면,
class MyClass {
public:
// 1. 이동 생성자에 noexcept를 안 붙인 경우 -> 예외 발생 가능성을 염려해 복사가 진행
MyClass(MyClass&& rhs) { ... }
// 2. 이동 생성자에 noexcept를 붙인 경우 -> 복사가 아닌 이동(Move)으로 진행
MyClass(MyClass&& rhs) noexcept { ... }
};
이동 생성자라 하더라도 1번의 경우는 복사로 진행된다. 코드 작업 중에 예외가 발생할 수 있기 때문이다. 느리다.
하지만 2번의 경우는 noexcept로 예외가 발생하지 않음을 보장하기에 이동(Move)으로 처리된다. 빠르다.
즉, std::vector는 "이동생성자가 noexcept로 선언되어 있을 때만" 이동을 수행하고, 그렇지 않으면 안전하게 복사를 선택한다.
이런 이유로 STL내부에는 std::move_if_noexcept 라는 장치가 있다. 말 그대로 "예외 없다고 보장할 수 있으면 이동하고 아니면 복사하라"는 의미이다.
---
5. swap과 조건부 noexcept의 연쇄 반응
swap은 알고리즘의 기초이다. 이 책에선 조건부 noexcept의 중요성을 언급한다.
template <class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));
의미 : "배열을 swap 할 때 예외가 안 터지는지는, 배열의 요소(T)를 swap 할 때 예외가 터지는지에 달려있다"
-> 이처럼 낮은 단계의 함수(사용자 정의 swap)가 noexcept를 잘 지켜줘야, 그 위에 쌓이는 std::pair, std::vector 등의 상위 구조체들도 연쇄적으로 noexcept 혜택을 받아 최적화 될 수 있다.
---
6. 넓은 계약(Wide) vs 좁은 계약(Narrow)
라이브러리 설계시 고려해야 할 점을 설명하고 있다.
- 넓은 계약 : 전제 조건이 없음. 어떤 인자가 들어와도 정의된 동작을 수행함. 예외가 없다면 noexcept를 붙이기 쉽다.
- 좁은 계약 : 특정 전제 조건이 있음(예 : s.length() <= 32).
- 조건을 어기면 정의되지 않은 동작(UB)이 발생한다.
- 설계자들은 좁은 계약 함수에 noexcept를 붙이는 것을 꺼리기도 한다. 디버깅을 위해 전제 조건 위반 시 예외를 던지고 싶을 수 있기 때문이다. noexcept를 붙이는 순간 프로그램이 죽어버리니까 좁은 계약 함수엔 noexcept를 잘 붙이지 않는다.
---
7. 주의사항
함부로 noexcept를 붙여선 안된다. 정확성이 최적화보다 중요하다.
1. 인터페이스 변경의 위험 : 한 번 noexcept로 선언했다가 나중에 예외를 던지도록 바꾸면 사용자 코드가 깨진다. 클라이언트 코드가 이미 최적화된 로직에 의존하고 있기 때문이다.
2. 예외 중립성(Exception-neutral) : 대부분의 함수는 자기가 부른 함수가 던진 예외를 그대로 통과시키는 '예외 중립적' 상태이다. 이런 함수는 noexcept가 될 수 없다.
3. noexcept를 붙일려고 예외 대신 상태 코드를 반환하는 식으로 코드를 꼬는 것은 소프트웨어 공학적으로 나쁜 선택이다.
---
8. 자동으로 noexcept가 되는 것들
C++11에서는 스타일 가이드가 언어 규칙이 된 사례가 있다.
- 소멸자(Destructor)와 메모리 해제 함수(delete) : 기본적으로 모두 noexcept 이다.
- 소멸자에서 예외가 나가는 것은 매우 위험하므로, 명시적으로 noexcept(false) 라고 하지 않는 한 컴파일러가 알아서 noexcept를 붙여준다.
---
9. 결론.
컴파일러는 noexcept 함수 내부에서 noexcept가 아닌 함수(예 : C언어 함수 strlen 등, 예외를 던질 수 있는 함수)를 호출해도 컴파일러는 경고를 주지 않는다. 이로 인해 발생하는 문제는 설계자의 책임이다.
---
10. 요약
- noexcept는 인터페이스의 일부이며, 사용자와 컴파일러 모두에게 주는 강력한 약속이다.
- 이동 연산, swap, 소멸자에는 반드시 noexcept를 고려하라. 성능 향상의 폭이 압도적이다.
- 대부분의 함수는 '예외 중립적(Exception-neutral)'이므로 무분별한 noexcept 남발은 독이 된다.
- 최적화보다 중요한 것은 보증할 수 있는 정확성이다.
'읽은 기록 > Effective Modern C++' 카테고리의 다른 글
| [CH3] Item16. Make const member functions thread safe. (0) | 2026.03.28 |
|---|---|
| [CH3] Item15. Use constexpr whenever possible. (0) | 2026.03.22 |
| [CH3] Item13. Prefer const_iterators to iterators. (0) | 2026.03.15 |
| [CH3] Item12. Declare overriding functions override. (0) | 2026.03.10 |
| [CH3] Item11. Prefer deleted functions to private undefined ones. (0) | 2026.03.09 |