예외 안전성 수준(Basic/Strong/Nothrow)과 noexcept 설계 전략
개요 — 예외 안전성이란
Section titled “개요 — 예외 안전성이란”예외 안전성(Exception Safety)은 함수가 예외를 던질 때 프로그램이 유효한 상태를 유지하는 정도를 의미합니다. 예외가 발생해도 자원 누수, 데이터 손상, 불변식(invariant) 위반이 없어야 합니다.
예외 안전 코드를 작성하지 않으면:
- 메모리 누수 (delete가 실행되지 않음)
- 파일/소켓 핸들 미반환
- 객체가 절반만 수정된 비일관 상태(partial update)
- 데이터 손상 및 정의되지 않은 동작(UB)
1. 예외 안전성 세 가지 보장 수준
Section titled “1. 예외 안전성 세 가지 보장 수준”수준 비교 요약
Section titled “수준 비교 요약”| 수준 | 이름 | 의미 |
|---|---|---|
| 3 (최강) | Nothrow Guarantee | 예외를 절대 던지지 않음 — noexcept |
| 2 | Strong Guarantee | 예외 시 이전 상태로 완전 복구 (원자성) |
| 1 | Basic Guarantee | 예외 시 자원 누수 없고, 유효한 상태 유지 (값은 변할 수 있음) |
| 0 (최약) | No Guarantee | 예외 시 자원 누수, 데이터 손상 가능 |
2. Basic Guarantee — 최소 보장
Section titled “2. Basic Guarantee — 최소 보장”예외가 발생해도 자원이 누수되지 않고, 객체가 유효한 상태를 유지합니다. 단, 값이 부분적으로 변경될 수 있습니다.
class DataProcessor{ std::vector<int> data; int count;
public: // Basic Guarantee 수준 // 예외 발생 시 data는 부분적으로 변경될 수 있지만 유효한 상태 void AddAll(const std::vector<int>& items) { for (int item : items) { if (item < 0) throw std::invalid_argument("음수는 허용되지 않습니다"); data.push_back(item); // 예외 전까지 추가된 원소는 남음 ++count; } }};// 문제: items가 [1, 2, -3, 4]이면// 예외 후 data = {1, 2}, count = 2 — 부분 수정됨3. Strong Guarantee — 원자성 보장
Section titled “3. Strong Guarantee — 원자성 보장”예외 발생 시 작업이 전혀 수행되지 않은 것처럼 이전 상태로 완전히 복구됩니다. “Commit or Rollback” 의미론입니다.
Copy-and-Swap 관용구
Section titled “Copy-and-Swap 관용구”Strong Guarantee를 구현하는 가장 일반적인 패턴입니다.
class DataStore{ std::vector<int> data;
public: // Strong Guarantee: 복사본에서 작업 → 성공 시 swap void ReplaceAll(const std::vector<int>& new_data) { // 1. 복사본 생성 (예외가 여기서 발생해도 원본 불변) std::vector<int> temp = new_data;
// 2. 복사본에서 추가 처리 (예외 발생 가능) for (auto& v : temp) { if (v < 0) throw std::invalid_argument("음수 불허"); v *= 2; }
// 3. 모든 처리 성공 → noexcept swap으로 원자적 교체 std::swap(data, temp); // swap은 throw 안 함 }
// 대입 연산자 — Copy-and-Swap DataStore& operator=(DataStore other) // 값으로 받아 복사 수행 { std::swap(data, other.data); return *this; }};Strong Guarantee가 어려운 경우
Section titled “Strong Guarantee가 어려운 경우”// 두 컨테이너를 동시에 수정하면 Strong Guarantee 구현 어려움void Transfer(std::vector<int>& src, std::vector<int>& dst, int index){ dst.push_back(src[index]); // push_back이 예외 던지면? src.erase(src.begin() + index); // dst는 이미 변경됨 → 불일치}
// Strong Guarantee 버전void TransferSafe(std::vector<int>& src, std::vector<int>& dst, int index){ std::vector<int> dst_copy = dst; // 복사 dst_copy.push_back(src[index]); // 복사본 수정 (예외 가능) // 여기까지 성공 → 원자적 교체 std::swap(dst, dst_copy); src.erase(src.begin() + index); // noexcept일 경우 안전}4. Nothrow Guarantee — noexcept
Section titled “4. Nothrow Guarantee — noexcept”noexcept로 지정된 함수는 예외를 절대 던지지 않음을 약속합니다. 예외가 발생하면 std::terminate()가 호출됩니다.
// noexcept 지정 — 예외를 던지지 않겠다는 계약void Swap(int& a, int& b) noexcept{ int tmp = a; a = b; b = tmp;}
// noexcept 조건부 지정 — 다른 함수가 noexcept일 때만 noexcepttemplate<typename T>void SafeSwap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T> && std::is_nothrow_move_assignable_v<T>){ T tmp = std::move(a); a = std::move(b); b = std::move(tmp);}noexcept가 중요한 이유 — std::vector 재할당 최적화
Section titled “noexcept가 중요한 이유 — std::vector 재할당 최적화”class MyClass{public: // move 생성자가 noexcept이어야 vector 재할당 시 move 사용 MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {} MyClass& operator=(MyClass&& other) noexcept { data = std::move(other.data); return *this; }
private: std::vector<int> data;};
// noexcept 없으면 vector::push_back 시 재할당에서 copy 사용 (느림)// noexcept 있으면 move 사용 (빠름)std::vector<MyClass> vec;vec.push_back(MyClass{}); // noexcept move가 없으면 copy로 fallback5. noexcept 지정 우선순위
Section titled “5. noexcept 지정 우선순위”// 반드시 noexcept여야 하는 함수들class Resource{public: // 1. 소멸자 — 기본적으로 noexcept (암묵적) ~Resource() noexcept; // 명시적으로도 표기 권장
// 2. swap — noexcept이어야 Copy-and-Swap 패턴이 Strong Guarantee void swap(Resource& other) noexcept;
// 3. move 생성자/대입 — vector 재할당 최적화 Resource(Resource&&) noexcept; Resource& operator=(Resource&&) noexcept;
// 4. 기본 생성자 — 가능하면 noexcept Resource() noexcept = default;};noexcept 체크 — noexcept 연산자
Section titled “noexcept 체크 — noexcept 연산자”void f() noexcept {}void g() {}
static_assert(noexcept(f())); // true — f는 noexceptstatic_assert(!noexcept(g())); // false — g는 noexcept 아님
// 타입 특성으로 체크static_assert(std::is_nothrow_move_constructible_v<std::vector<int>>);static_assert(std::is_nothrow_destructible_v<int>);6. RAII로 예외 안전성 구현
Section titled “6. RAII로 예외 안전성 구현”RAII(Resource Acquisition Is Initialization)는 예외 안전 코드의 기반입니다. 소멸자가 반드시 호출되므로 자원이 항상 해제됩니다.
// 잘못된 예 — 예외 시 자원 누수void BadFunction(){ int* ptr = new int[100]; RiskyOperation(); // 예외 발생 시 delete[] 미실행 → 누수 delete[] ptr;}
// RAII로 수정void GoodFunction(){ auto ptr = std::make_unique<int[]>(100); // RAII RiskyOperation(); // 예외 발생해도 ptr 소멸자가 자동 해제}
// 파일 핸들 RAIIclass FileGuard{ FILE* file;public: explicit FileGuard(const char* path) : file(fopen(path, "r")) { if (!file) throw std::runtime_error("파일 열기 실패"); } ~FileGuard() { if (file) fclose(file); } // 예외 시에도 보장 FILE* Get() const { return file; }};7. 예외 안전 설계 체크리스트
Section titled “7. 예외 안전 설계 체크리스트”class SafeBuffer{ std::unique_ptr<int[]> data; // raw pointer 대신 smart pointer size_t size; size_t capacity;
public: // Strong Guarantee: copy-and-swap void Resize(size_t new_capacity) { if (new_capacity == capacity) return;
// 새 버퍼 할당 (예외 가능 — 원본 불변) auto new_data = std::make_unique<int[]>(new_capacity);
// 데이터 복사 (예외 가능 — 원본 불변) size_t copy_count = std::min(size, new_capacity); std::copy_n(data.get(), copy_count, new_data.get());
// 성공 후 noexcept swap으로 교체 std::swap(data, new_data); capacity = new_capacity; size = copy_count; }
// Nothrow Guarantee: 인덱스 접근 int& operator[](size_t i) noexcept { return data[i]; }
// Basic Guarantee: push_back void PushBack(int value) { if (size >= capacity) Resize(capacity ? capacity * 2 : 1); // Strong data[size++] = value; // noexcept }};8. 예외 명세 진화
Section titled “8. 예외 명세 진화”| 시대 | 문법 | 의미 |
|---|---|---|
| C++03 | throw(T) | T 타입만 던질 수 있음 (동적 명세) |
| C++03 | throw() | 예외를 던지지 않음 |
| C++11 | noexcept | 예외를 던지지 않음 (정적, 성능 최적화) |
| C++11 | noexcept(expr) | expr이 true일 때만 noexcept |
| C++17 | throw(T) 제거 | 동적 예외 명세 폐기 |
Nothrow (noexcept) ↑ 소멸자, swap, move 연산에 반드시 적용Strong Guarantee ↑ Copy-and-Swap 패턴으로 구현 ↑ 원자성이 필요한 상태 변경에 적용Basic Guarantee ↑ RAII로 자원 누수 방지 ↑ 최소한의 예외 안전성 — 항상 달성해야 함No Guarantee ✗ 예외 미처리 — 작성 금지핵심 규칙:
- 소멸자, swap, move 연산은 반드시
noexcept로 선언합니다. - 상태 변경은 Copy-and-Swap으로 Strong Guarantee를 달성합니다.
- 자원 관리에는 항상 RAII(스마트 포인터, 핸들 래퍼)를 사용합니다.
noexcept가 없는 move 생성자는std::vector재할당 시 copy로 퇴보합니다.