← 문제로

9. 예외 안전성과 noexcept, vector 재할당의 move_if_noexcept

난이도 최상
내 리뷰 · C++
해설 · C++

해설 — 예외 안전성과 noexcept, vector 재할당의 move_if_noexcept

난이도: 최상

요약

std::vector 가 재할당하며 기존 원소를 새 버퍼로 옮길 때, 원소의 이동 생성자가 noexcept 면 이동, 아니면 복사를 택한다. 그 판단을 하는 도구가 std::move_if_noexcept 다. 이유는 push_back/emplace_back 이 제공해야 하는 강한 예외 안전성 때문이다: 재할당 도중 원소를 옮기다 예외가 나면, 이동은 원본을 이미 망가뜨렸을 수 있어 원래 상태로 되돌릴 수 없다. 그래서 "던질 수 있는 이동"이면 안전하게 복사(원본 보존)로 후퇴한다.

실행 결과

[Throwing]
-- trigger realloc --
copy            // 이동 생성자에 noexcept 가 없으니 재할당은 복사로 이전
copy
[NoexceptMove]
-- trigger realloc --
move            // 이동 생성자가 noexcept 이므로 이동으로 이전
move

(처음 두 emplace_back 의 기본 생성은 출력이 없어 보이지 않는다. 출력 줄은 재할당 시 기존 2개 원소의 이전뿐이다.)

핵심 개념

1. 강한 예외 안전성과 재할당

push_back 은 강한 보장을 약속한다: 성공하면 추가, 예외가 나면 벡터는 호출 전과 동일. 재할당은 (1) 새 버퍼 할당 → (2) 기존 원소 새 버퍼로 이전 → (3) 옛 버퍼 파괴, 순서다. 만약 (2)에서 k번째 원소를 옮기다 예외가 나면, 이미 옮긴 0..k-1 과 남은 것들의 상태를 원복해야 한다.

  • 복사로 이전: 원본은 그대로 남으므로, 예외 시 새 버퍼만 버리면 옛 버퍼(원본)가 멀쩡 → 강한 보장 유지.
  • 이동으로 이전: 옮기다 예외가 나면 이미 이동된 원소의 원본은 "비워진" 상태라 원복 불가 → 강한 보장 깨짐.

2. move_if_noexcept 의 결정

vector 의 재할당 루프는 각 원소를 std::move_if_noexcept(elem) 로 넘긴다. 이 함수는:

  • 이동 생성자가 noexcept(또는 복사 생성자가 없음) → T&&(이동) 반환.
  • 그렇지 않으면 → const T&(복사) 반환. 즉 "이동이 절대 안 던진다고 보장될 때만" 이동을 쓰고, 아니면 복사로 강한 보장을 산다. Throwing 은 이동이 noexcept 가 아니라 복사, NoexceptMove 는 noexcept 라 이동.

3. noexcept 는 성능 계약이다

이동 생성자에 noexcept 를 붙이는 것은 단순 표식이 아니라, 표준 컨테이너가 이동 최적화를 쓸 수 있게 하는 계약이다. 빠뜨리면 재할당마다 비싼 복사가 일어난다(특히 std::string/std::vector 멤버를 가진 무거운 타입). 단, noexcept 라 선언해 놓고 실제로 던지면 std::terminate 로 직행하므로 거짓 약속은 금물.

흔한 오해·함정

  • "이동 생성자를 정의했으니 vector 가 이동하겠지": noexcept 가 없으면 재할당은 복사한다. 사용자 정의 이동 생성자에 noexcept 를 빼먹는 게 가장 흔한 성능 함정.
  • 컴파일러 생성 이동 생성자: 멤버들이 모두 noexcept 이동이면 자동 생성 이동도 noexcept 가 된다(암묵적 noexcept 추론). 직접 정의하면 추론이 끊기니 명시해야 한다.
  • vector::resize/insert 도 동일 규칙. reserve 로 재할당 횟수를 줄이면 이 비용 자체가 줄어든다.
  • basic vs strong: 모든 연산이 강한 보장을 주진 않는다. 강한 보장이 비싸면 basic(누수 없음·불변식 유지)만 약속하기도 한다 — API 의 보장 수준을 알고 써야 한다.

면접 포인트

  1. vector 재할당은 이동 생성자가 noexcept 일 때만 이동, 아니면 복사한다(std::move_if_noexcept). 이유는 push_back강한 예외 안전성 보장.
  2. 따라서 사용자 정의 이동 생성자/이동 대입에는 가능하면 noexcept 를 붙여야 컨테이너가 이동 최적화를 쓴다 — 게임서버의 핫한 벡터(엔티티/패킷 버퍼)에서 큰 성능 차이.
  3. 예외 안전성 보장은 basic/strong/nothrow 3단계로 구분되며, 표준 라이브러리는 이 계약에 맞춰 동작(여기선 strong 을 지키려 복사 후퇴)을 선택한다.