본문 바로가기

예외 던지기

throw할 예외 타입과 정보를 설계하고 예외 객체 전달·스택 풀기·RAII 자원 정리가 이어지는 흐름을 이해합니다.

예외 상황이 발생했음을 시스템에 알리고, 그에 해당하는 예외 객체를 생성하여 던지는(throw) 방법을 다룹니다.

throw 문은 예외 처리 메커니즘의 시작점이며, 어떤 타입의 객체를 던지느냐에 따라 catch 블록의 동작이 결정됩니다.

예외를 값으로 던지고 const reference로 받으며 RAII로 자원을 정리한다

업무 호출→FileHandler 획득→값 throw→unwind 해제→const& catch의 자원 생명 주기를 시간순으로 읽을 수 있다.

C++ · EXCEPTION OWNERSHIP

값으로 throw하고 const 참조로 catch하며 RAII로 정리한다

예외 객체는 throw 표현식으로 초기화되고 handler의 const reference에 바인딩된다. 중간 프레임은 값을 복사해 전달하는 대신 unwind하며 소유 자원을 정리한다.

예외는 값으로 던지고 const reference로 받아 RAII 수명을 보존한다본문처럼호출자와catch는동일복구경계이며두역할을한lifeline에묶었다. 세역할다섯메시지:호출자→업무call,업무→FileHandler획득,업무self메시지throw값,업무→FileHandlerunwind중release,업무→호출자catchconst참조바인딩이다. 시간은위에서아래. release는이미쓴파일내용의rollback이아니고bind는예외의물리적복사횟수를뜻하지않는다. 모바일도같은세역할다섯메시지이며FileHandler식별자줄만나눔. 호출자 · 복구 경계catch const&업무 함수throw by valueFileHandler자원 소유callacquirereleasebind const&throw value
예외는 값으로 던지고 const reference로 받아 RAII 수명을 보존한다본문처럼호출자와catch는동일복구경계이며두역할을한lifeline에묶었다. 세역할다섯메시지:호출자→업무call,업무→FileHandler획득,업무self메시지throw값,업무→FileHandlerunwind중release,업무→호출자catchconst참조바인딩이다. 시간은위에서아래. release는이미쓴파일내용의rollback이아니고bind는예외의물리적복사횟수를뜻하지않는다. 모바일도같은세역할다섯메시지이며FileHandler식별자줄만나눔.호출자catchconst&업무함수값으로throwFileHandler자원 소유callacquirereleasebind const&값으로throw
  1. 의미 있는 타입

    std::invalid_argument처럼 호출자가 구분할 수 있는 타입을 고릅니다.

  2. 값으로 throw

    지역 객체의 주소나 C 문자열이 아니라 완결된 예외 객체를 던집니다.

  3. RAII 정리

    중간 프레임의 guard가 unwind 중에도 정확히 한 번 자원을 해제합니다.

  4. const reference catch

    복사와 slicing 없이 구체 예외의 정보를 읽습니다.

예외 객체의 물리적 복사 횟수나 stdout/stderr의 상대 출력 순서를 계약으로 삼지 않는다. 호출자와 catch는 같은 복구 경계의 두 역할로 묶었다. release는 파일을 닫는 자원 정리이며 이미 쓴 데이터의 취소가 아니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목예외를 값으로 던지고 const reference로 받으며 RAII로 자원을 정리한다
관계·조건 설명본문처럼 호출자와 catch는 동일 복구 경계이며 두 역할을 한 lifeline에 묶었다. 세 역할의 다섯 메시지는 호출자→업무 call, 업무→FileHandler 획득, 업무 self 메시지 throw 값, 업무→FileHandler unwind 중 release, 업무→호출자 catch const 참조 바인딩이다. 시간은 위에서 아래로 흐른다. release는 이미 쓴 파일 내용의 rollback이 아니고 bind는 예외의 물리적 복사 횟수를 뜻하지 않는다. 모바일도 같은 세 역할의 다섯 메시지이며 FileHandler 식별자 줄만 나눈다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
FileHandler 획득

호출된 업무 함수가 파일 자원을 관리한다.

  • 소유자: FileHandler
  • 자원: 획득됨
  • 자동 객체의 수명이 정리 책임을 묶는다.
값 throw와 unwind

예외가 전달되며 벗어난 범위의 파일 자원이 정리된다.

  • 예외: 값으로 throw
  • FileHandler: 소멸·자원 해제
  • 스택을 벗어나는 완성된 소유 객체의 소멸자가 실행된다.
참조로 catch

상위 경계가 const 참조로 예외를 읽는다.

  • catch: const&
  • 파일 자원: 이미 해제됨
  • handler의 예외 읽기와 파일의 정리 책임은 별개다.

예외를 값으로 던지고 const reference로 받으며 RAII로 자원을 정리한다

업무 호출→FileHandler 획득→값 throw→unwind 해제→const& catch의 자원 생명 주기를 시간순으로 읽을 수 있다.

C++ · EXCEPTION OWNERSHIP

값으로 throw하고 const 참조로 catch하며 RAII로 정리한다

예외 객체는 throw 표현식으로 초기화되고 handler의 const reference에 바인딩된다. 중간 프레임은 값을 복사해 전달하는 대신 unwind하며 소유 자원을 정리한다.

예외는 값으로 던지고 const reference로 받아 RAII 수명을 보존한다본문처럼호출자와catch는동일복구경계이며두역할을한lifeline에묶었다. 세역할다섯메시지:호출자→업무call,업무→FileHandler획득,업무self메시지throw값,업무→FileHandlerunwind중release,업무→호출자catchconst참조바인딩이다. 시간은위에서아래. release는이미쓴파일내용의rollback이아니고bind는예외의물리적복사횟수를뜻하지않는다. 모바일도같은세역할다섯메시지이며FileHandler식별자줄만나눔. 호출자 · 복구 경계catch const&업무 함수throw by valueFileHandler자원 소유callacquirereleasebind const&throw value
예외는 값으로 던지고 const reference로 받아 RAII 수명을 보존한다본문처럼호출자와catch는동일복구경계이며두역할을한lifeline에묶었다. 세역할다섯메시지:호출자→업무call,업무→FileHandler획득,업무self메시지throw값,업무→FileHandlerunwind중release,업무→호출자catchconst참조바인딩이다. 시간은위에서아래. release는이미쓴파일내용의rollback이아니고bind는예외의물리적복사횟수를뜻하지않는다. 모바일도같은세역할다섯메시지이며FileHandler식별자줄만나눔.호출자catchconst&업무함수값으로throwFileHandler자원 소유callacquirereleasebind const&값으로throw
  1. 의미 있는 타입

    std::invalid_argument처럼 호출자가 구분할 수 있는 타입을 고릅니다.

  2. 값으로 throw

    지역 객체의 주소나 C 문자열이 아니라 완결된 예외 객체를 던집니다.

  3. RAII 정리

    중간 프레임의 guard가 unwind 중에도 정확히 한 번 자원을 해제합니다.

  4. const reference catch

    복사와 slicing 없이 구체 예외의 정보를 읽습니다.

예외 객체의 물리적 복사 횟수나 stdout/stderr의 상대 출력 순서를 계약으로 삼지 않는다. 호출자와 catch는 같은 복구 경계의 두 역할로 묶었다. release는 파일을 닫는 자원 정리이며 이미 쓴 데이터의 취소가 아니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목예외를 값으로 던지고 const reference로 받으며 RAII로 자원을 정리한다
관계·조건 설명본문처럼 호출자와 catch는 동일 복구 경계이며 두 역할을 한 lifeline에 묶었다. 세 역할의 다섯 메시지는 호출자→업무 call, 업무→FileHandler 획득, 업무 self 메시지 throw 값, 업무→FileHandler unwind 중 release, 업무→호출자 catch const 참조 바인딩이다. 시간은 위에서 아래로 흐른다. release는 이미 쓴 파일 내용의 rollback이 아니고 bind는 예외의 물리적 복사 횟수를 뜻하지 않는다. 모바일도 같은 세 역할의 다섯 메시지이며 FileHandler 식별자 줄만 나눈다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
FileHandler 획득

호출된 업무 함수가 파일 자원을 관리한다.

  • 소유자: FileHandler
  • 자원: 획득됨
  • 자동 객체의 수명이 정리 책임을 묶는다.
값 throw와 unwind

예외가 전달되며 벗어난 범위의 파일 자원이 정리된다.

  • 예외: 값으로 throw
  • FileHandler: 소멸·자원 해제
  • 스택을 벗어나는 완성된 소유 객체의 소멸자가 실행된다.
참조로 catch

상위 경계가 const 참조로 예외를 읽는다.

  • catch: const&
  • 파일 자원: 이미 해제됨
  • handler의 예외 읽기와 파일의 정리 책임은 별개다.

throw 문: 예외 발생 알림

throw 문은 현재 프로그램의 실행 흐름이 더 이상 정상적으로 진행될 수 없음을 알리고, 제어를 catch 블록으로 옮기는 역할을 합니다.

throw 키워드 뒤에는 던지고자 하는 예외 객체(Exception Object)를 명시합니다.

throw 문 형식
throw 표현식;
  • 표현식: 어떤 타입의 객체든 될 수 있습니다. 이 객체가 catch 블록으로 전달되어 예외 정보로 활용됩니다.
throw 문의 동작

throw 문이 실행되면, 표현식의 결과로 임시 예외 객체가 생성됩니다.

현재의 정상 실행 경로가 중단되어, 예외가 던져진 뒤의 일반 문장을 건너뜁니다.

가장 가까운 활성 try부터 해당 예외를 처리할 수 있는 catch 블록을 찾고, 없으면 바깥의 활성 try로 탐색을 이어갑니다. 같은 함수의 catch가 정상적으로 끝나면 그 함수의 다음 문장으로 이어질 수 있습니다.

이때 벗어나는 범위의 완전히 생성된 자동 객체는 생성의 역순으로 소멸합니다. 이 정리 과정을 스택 풀기(Stack Unwinding)라고 합니다.

적절한 catch 블록이 발견되면, 해당 catch 블록으로 제어가 넘어갑니다.

main 함수에 도달해도 어떤 catch 블록도 예외를 처리하지 못하면, 프로그램은 std::terminate 함수를 호출하며 비정상적으로 종료됩니다.


어떤 것을 throw로 던질 것인가?

C++에서는 기본 타입과 클래스 객체 등 다양한 값을 던질 수 있지만, 불완전 타입이나 추상 클래스 등은 예외 객체로 만들 수 없습니다.

정수, 문자열 리터럴(const char*), std::string 객체, 그리고 사용자 정의 클래스의 객체 등 무엇이든 던질 수 있습니다.

기본 타입의 값(int, double)과 const char 포인터 던지기: 간단한 예외 상황이나 디버깅 목적으로 사용할 수 있습니다.

기본 타입 예외 던지기
#include <iostream>

void divide(int a, int b) {
    if (b == 0) {
        throw 100; // int 타입의 에러 코드 던지기
    }
    std::cout << "Result: " << a / b << std::endl;
}

void processFile(const char* filename) {
    if (filename == nullptr || filename[0] == '\0') {
        throw "Filename cannot be empty!"; // const char* (C-스타일 문자열) 던지기
    }
    // 실제 파일 처리 로직...
    std::cout << "File '" << filename << "' processed successfully.\n";
}

int main() {
    try {
        divide(10, 0);
    } catch (int e) {
        std::cerr << "Caught integer error code: " << e << std::endl;
    }

    try {
        processFile("");
    } catch (const char* msg) {
        std::cerr << "Caught string error: " << msg << std::endl;
    }
    return 0;
}

주의: 기본 타입의 값을 던지는 것은 예외에 대한 정보를 충분히 제공하기 어렵고, 계층적인 예외 처리를 어렵게 하므로 일반적으로 권장되지 않습니다.

std::string 객체 던지기: 오류 메시지를 담는 데 유용하지만, std::exception 계층 구조에 포함되지 않으므로 다형성을 활용하기 어렵습니다.

std::string 예외 던지기
#include <iostream>
#include <string>

void connectToServer(const std::string& address) {
    if (address.empty()) {
        throw std::string("Connection Error: Server address is empty!"); // std::string 객체 던지기
    }
    // 실제 연결 로직...
    std::cout << "Connected to " << address << std::endl;
}

int main() {
    try {
        connectToServer("");
    } catch (const std::string& e) {
        std::cerr << "Connection Failed: " << e << std::endl;
    }
    return 0;
}

표준 예외 클래스 객체 던지기 (std::exception 파생 클래스): 가장 권장되는 방법입니다.

C++ 표준 라이브러리는 <stdexcept> 헤더에 다양한 표준 예외 클래스를 제공합니다.

이들은 모두 std::exception 클래스를 상속받습니다.

  • std::exception: 모든 표준 예외의 최상위 기반 클래스. what() 멤버 함수를 통해 오류 메시지를 얻을 수 있습니다.
  • std::logic_error: 프로그램의 논리적 오류 (예: std::invalid_argument, std::out_of_range)
  • std::runtime_error: 프로그램 실행 중 발생할 수 있는 오류 (예: std::overflow_error, std::underflow_error, std::range_error)
표준 예외 클래스 예시
#include <iostream>
#include <stdexcept> // std::invalid_argument, std::out_of_range, std::runtime_error
#include <vector>

void process_data(int data) {
    if (data < 0) {
        throw std::invalid_argument("Data cannot be negative."); // 논리 오류
    }
    if (data > 100) {
        throw std::out_of_range("Data is too large, must be <= 100."); // 범위 오류
    }
    std::cout << "Data processed: " << data << std::endl;
}

void simulate_hardware_failure() {
    bool failure_detected = true; // 실제로는 하드웨어 상태를 검사
    if (failure_detected) {
        throw std::runtime_error("Hardware malfunction detected!"); // 런타임 오류
    }
    std::cout << "Hardware operating normally.\n";
}

int main() {
    // std::invalid_argument 예외 처리
    try {
        process_data(-5);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Caught invalid_argument: " << e.what() << std::endl;
    }

    // std::out_of_range 예외 처리
    try {
        process_data(150);
    } catch (const std::out_of_range& e) {
        std::cerr << "Caught out_of_range: " << e.what() << std::endl;
    }

    // std::runtime_error 예외 처리
    try {
        simulate_hardware_failure();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught runtime_error: " << e.what() << std::endl;
    }

    // 일반 std::exception으로 모든 표준 예외를 잡기
    try {
        // 다시 던져서 테스트
        // process_data(-10); // 주석 해제 시 위에서 잡힘
        std::vector<int> v(5);
        v.at(10) = 1; // std::out_of_range 발생
    } catch (const std::exception& e) { // std::out_of_range는 std::exception을 상속하므로 여기서 잡힘
        std::cerr << "Caught generic std::exception: " << e.what() << std::endl;
    }

    return 0;
}

표준 예외 클래스를 사용하면 예외에 대한 의미론적인 분류가 가능하며, 계층 구조를 통해 다형적인 예외 처리가 용이합니다.

사용자 정의 예외 클래스 객체 던지기: 가장 유연한 방법입니다.

프로그램 고유의 오류에 대한 상세한 정보를 제공하고 싶을 때 사용합니다.

일반적으로 std::exception을 상속받아 what() 메서드를 오버라이드하여 사용자 정의 오류 메시지를 제공합니다. (다음 장에서 자세히 다룹니다.)

사용자 정의 예외 클래스 예시
// MyCustomException.h (예시, 다음 장에서 자세히)
#include <iostream>
#include <stdexcept>
#include <string>

class MyCustomException : public std::runtime_error {
public:
    MyCustomException(const std::string& message)
        : std::runtime_error(message) {}
};

// 사용 예시
void process_network_packet(int packet_size) {
    if (packet_size < 10) {
        throw MyCustomException("Packet size is too small.");
    }
    if (packet_size > 1024) {
        throw MyCustomException("Packet size exceeds maximum limit.");
    }
    std::cout << "Packet processed, size: " << packet_size << std::endl;
}

int main() {
    try {
        process_network_packet(5);
    } catch (const MyCustomException& e) {
        std::cerr << "Caught custom exception: " << e.what() << std::endl;
    } catch (const std::exception& e) { // MyCustomException도 std::exception을 상속하므로 여기서도 잡힐 수 있음
        std::cerr << "Caught general exception: " << e.what() << std::endl;
    }
    return 0;
}

throw 시 객체 복사 vs 참조

예외를 던질 때, 던져지는 객체는 throw 문에 의해 예외 객체로 초기화되어 예외 처리 메커니즘으로 전달됩니다. 초기화 과정의 복사·이동과 생략 여부는 식과 언어 규칙에 따라 달라지므로 물리적인 복사 횟수를 고정하지 않습니다. 아래 const MyException&로 받는 것은 handler 매개변수의 추가 값 복사를 피하는 선택입니다.

따라서 예외 객체를 const MyException&와 같이 const 참조로 잡는 것이 일반적으로 권장됩니다.

이는 불필요한 복사 비용을 줄이고, 던져진 객체의 실제 타입이 보존되어 다형성을 활용할 수 있게 합니다.

예외 객체 던지기와 잡기
// 올바른 예시: 값으로 던지고, const 참조로 잡기
throw MyException("Error details."); // 값으로 던짐
// ...
catch (const MyException& e) { // const 참조로 잡음
    // 안전하고 효율적
}

// 잘못된 예시: 포인터로 던지고, 포인터로 잡기 (메모리 관리 문제)
// throw new MyException("Error details."); // new로 동적 할당된 객체 던짐
// // ...
// catch (MyException* e) { // 포인터로 잡음
//     delete e; // catch 블록에서 명시적으로 delete 해줘야 함 (번거롭고 실수할 가능성 높음)
// }
// C++에서는 예외를 포인터로 던지는 것을 강하게 비권장합니다.
// 예외 객체의 생명 주기는 시스템이 관리해야 합니다.

예외 발생 시 스택 풀기와 RAII

throw로 예외가 던져지면 처리할 catch를 찾고, 그 catch에 도달하기 위해 벗어나는 범위를 정리합니다. 같은 함수 안에서 처리된다면 그 함수의 스택 프레임 전체를 해제하는 것은 아닙니다.

이때 벗어나는 범위의 완전히 생성된 자동 객체는 생성의 역순으로 소멸자가 호출됩니다.

이를 스택 풀기(Stack Unwinding)라고 합니다.

이러한 스택 풀기 메커니즘은 RAII(Resource Acquisition Is Initialization) 패턴과 결합될 때 매우 강력해집니다.

RAII는 자원(메모리, 파일 핸들, 뮤텍스 등)을 객체에 캡슐화하고, 객체의 생성자에서 자원을 획득하며, 소멸자에서 자원을 해제하도록 설계하는 기법입니다.

RAII의 장점
  • 예외 안전성: 예외가 발생하여 스택 풀기가 일어나더라도, 스택에 있는 RAII 객체들의 소멸자가 자동으로 호출되어 자원이 안전하게 해제됩니다. 이는 자원 누수(Resource Leak)를 방지하는 핵심적인 방법입니다.
  • 코드 간결성: 자원 해제 코드를 명시적으로 작성할 필요가 줄어듭니다.
RAII를 통한 자원 안전성 보장
#include <iostream>
#include <string>
#include <stdexcept>
#include <fstream> // for std::ofstream

// RAII를 적용한 파일 핸들 관리 클래스
class FileHandler {
private:
    std::ofstream _file;
    std::string _filename;

public:
    FileHandler(const std::string& filename) : _filename(filename) {
        _file.open(filename);
        if (!_file.is_open()) {
            throw std::runtime_error("Failed to open file: " + filename);
        }
        std::cout << "FileHandler: File '" << _filename << "' opened.\n";
    }

    void write(const std::string& data) {
        if (!_file.is_open()) {
            throw std::runtime_error("File is not open for writing.");
        }
        _file << data << std::endl;
        if (!_file) throw std::runtime_error("Failed to write file: " + _filename);
        std::cout << "FileHandler: Wrote '" << data << "' to file.\n";
    }

    // 소멸자에서 파일 자동 닫기 (RAII 핵심)
    ~FileHandler() {
        if (_file.is_open()) {
            _file.close();
            std::cout << "FileHandler: File '" << _filename << "' closed.\n";
        }
    }
};

void manipulateFile(const std::string& filename, bool throw_error) {
    FileHandler handler(filename); // 파일 열기 (생성자)
    handler.write("First line.");

    if (throw_error) {
        throw std::runtime_error("Simulating an error during file manipulation."); // 예외 발생
    }

    handler.write("Second line (this might not be written).");
    std::cout << "File manipulation finished normally.\n";
}

int main() {
    std::cout << "--- Scenario 1: No exception ---\n";
    try {
        manipulateFile("output1.txt", false);
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    std::cout << "Scenario 1 finished.\n";

    std::cout << "\n--- Scenario 2: With exception ---\n";
    try {
        manipulateFile("output2.txt", true);
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    std::cout << "Scenario 2 finished.\n";

    return 0;
}
RAII 예시 실행 결과
--- Scenario 1: No exception ---
FileHandler: File 'output1.txt' opened.
FileHandler: Wrote 'First line.' to file.
FileHandler: Wrote 'Second line (this might not be written).' to file.
File manipulation finished normally.
FileHandler: File 'output1.txt' closed.
Scenario 1 finished.

--- Scenario 2: With exception ---
FileHandler: File 'output2.txt' opened.
FileHandler: Wrote 'First line.' to file.
FileHandler: File 'output2.txt' closed.
Caught exception: Simulating an error during file manipulation.
Scenario 2 finished.

위 출력은 파일 열기·쓰기가 성공했을 때의 예상 흐름입니다. stdout과 stderr를 따로 수집하면 표시 순서가 다를 수 있으나 소멸자는 catch 실행 전에 호출됩니다. Scenario 2에서 manipulateFile 함수 내부에 예외가 발생해도, FileHandler 객체의 소멸자가 호출되어 파일이 안전하게 닫히는 것을 확인할 수 있습니다.

이것이 RAII와 예외 처리의 중요한 시너지 효과입니다.

std::unique_ptr, std::shared_ptr, std::fstream 등 RAII 객체가 소유한 자원은 해당 객체의 수명과 함께 정리됩니다. 다만 파일을 닫는 것이 이미 쓴 내용을 취소하지는 않습니다. 원 Scenario 2에서 파일 열기·첫 쓰기가 성공했다면 output2.txt에는 First line.이 남고, throw 뒤의 Second line은 쓰이지 않습니다. 자원 누수 방지와 작업 전체의 원상 복구는 서로 다른 계약입니다.

생성자 자체가 실패했다면 그 완성되지 않은 객체의 소멸자는 호출되지 않으며, 그때까지 완전히 생성된 멤버와 기반 객체가 정리됩니다. 원 FileHandler에서 _file과 _filename은 생성자 본문보다 먼저 생성됩니다. 파일 열기 실패 때 FileHandler의 closed 로그가 없다고 해서 완성된 멤버의 정리가 모두 생략됐다고 판단하지 않습니다. 스택 풀기와 초기화 실패 시 정리를 참고하세요.

예시 다음 별도 C++17 파일은 try 밖의 객체와 try 안에서 벗어나는 객체를 구분합니다. 이름을 가진 Guard는 실제 파일 대신 생성·소멸 로그만 출력하며, 메모리 할당과 출력이 성공한다는 조건에서 순서를 추적합니다.

같은 함수의 catch가 남겨 두는 바깥 객체
#include <iostream>
#include <string>
#include <stdexcept>

struct Guard {
    std::string name;
    explicit Guard(std::string label) : name(label) {
        std::cout << "open " << name << '\n';
    }
    ~Guard() { std::cout << "close " << name << '\n'; }
};

void fail() {
    Guard deep("deep");
    throw std::runtime_error("boom");
}

int main() {
    Guard outer("outer");
    try {
        Guard inner("inner");
        fail();
        std::cout << "not reached\n";
    } catch (const std::exception& error) {
        std::cout << "caught " << error.what() << '\n';
    }
    std::cout << "after\n";
}

출력 순서는 open outer, open inner, open deep, close deep, close inner, caught boom, after, close outer입니다. deep과 inner는 빠져나오는 범위에 있어 역순으로 정리되지만, outer는 main의 catch 이후에도 살아 있습니다. main 자체가 끝날 때 outer가 소멸합니다. not reached는 예외가 발생한 호출 뒤에 있어 출력되지 않습니다.

스스로 수명 범위를 바꾸기 fail에서 throw를 Guard deep 생성보다 먼저 실행하도록 옮기면 위 출력에서 어떤 두 줄이 사라질까요? outer는 여전히 after 뒤에 닫히는지도 판단하세요. 예외를 catch에서 바깥으로 다시 보내려는 정책이라면 현재 예외를 보존하는 throw;와 새 예외를 만드는 throw error;도 같은 표현인지 설명해 보세요.

완성된 객체와 재던지기 풀이

open deep과 close deep이 사라집니다. deep 생성이 시작되기 전에 던졌으므로 정리할 완성된 deep 객체가 없습니다. inner는 이미 완성됐으므로 close inner가 catch 전에 실행되고, try 밖의 outer는 after 뒤에 정리됩니다. 현재 handler에서 throw;는 같은 예외 객체를 다시 보내지만, const std::exception& error를 throw error;로 던지면 식의 정적 기반 타입으로 새 예외 객체를 만들어 파생 정보가 잘릴 수 있습니다. 메시지 출력과 재던지기 중 어느 정책을 선택했는지 구분하고, 예외 객체와 재던지기를 확인합니다.

예외 객체를 고를 때는 전달 정보, 잡는 방식, 스택 풀기 중 자원 정리까지 함께 확인해야 합니다.

예외 던지기는 어떤 타입을 throw할지, 복사와 참조, 스택 풀기에서 RAII가 맡는 역할을 함께 정하는 설계 지점입니다.

처리할 handler를 찾지 못하거나 noexcept 경계를 넘으면 std::terminate가 호출될 수 있습니다. 그런 종료 경로에서 모든 지역 객체의 정리가 보장된다고 가정하지 않습니다.