본문 바로가기
프로그래밍/C & C++

[C++] Error Handling (2)

by 별준 2022. 3. 6.

References

Contents

  • Stack Unwinding and Cleanup (스택 풀기와 청소)
  • Common Error-Handling Issue
  • Function-Try-Blocks

[C++] Error Handling (1)

지난 포스팅에 이어서 C++의 에러 핸들링에 대해 알아보도록 하겠습니다.

 

 


5. Stack Unwinding and Cleanup

어떤 코드가 익셉션을 던지면 이를 받아서 처리할 catch 핸들러를 스택에서 찾습니다. 이때 catch 핸들러는 현재 스택 프레임에 바로 있을 수도 있고, 몇 단계의 함수 호출 스택을 거슬러 올라가야 나타날 수도 있습니다. 어떻게든 catch 핸들러를 발견하면 그 핸들러가 정의된 스택 단계로 되돌아가는데, 이 과정에서 중간 단계에 있던 스택 프레임을 모두 풀어버립니다. 이를 스택 풀기(stack unwinding)라 부르며, 스코프가 로컬인 소멸자를 모두 호출하고, 각 함수에서 미처 실행하지 못한 코드는 건너뜁니다.

 

하지만 스택 풀기가 발생할 때 포인터 변수를 해제하고 리소스를 정리하는 작업은 실행되지 않습니다. 따라서 다음과 같은 문제가 발생할 수 있습니다.

void funcOne();
void funcTwo();

int main()
{
    try {
        funcOne();
    }
    catch (const std::exception& e) {
        std::cerr << "Exception caught!\n";
        return 1;
    }
}

void funcOne()
{
    std::string str1;
    std::string* str2{ new std::string{} };
    funcTwo();
    delete str2;
}

void funcTwo()
{
    std::ifstream fileStream;
    fileStream.open("filename");
    throw std::exception{};
    fileStream.close();
}

funcTwo()에서 익셉션을 던질 때 가장 가까운 핸들러는 main()에 있습니다. 그래서 실행 흐름은 즉시 funcTwo()에 있던 throw std::exception{}; 문장에서 std::cerr << "Exception caugh!\n";로 건너뜁니다.

funcTwo()의 실행 지점은 익셉션을 던진 문장에 여전히 머물러 있습니다. 따라서 그 뒤에 나온 다음 문장은 실행되지 않습니다.

fileStream.close();

다행 ifstream 소멸자는 호출됩니다. 이는 fileStream이 스택에 있는 로컬 변수이기 때문입니다. ifstream 소멸자가 파일을 대신 닫아주므로 리소스 누수는 여기서 발생하지 않습니다. fileStream을 동적으로 할당했다면 제거되지 않기 때문에 파일은 닫히지 않고 그대로 남게 됩니다.

 

funcOne()에서 실행 지점이 funcTwo() 호출에 있으므로 그 뒤에 나온 다음 문장은 실행되지 않습니다.

delete str2;

따라서 여기서는 메모리 누수가 발생합니다. 스택 풀기 과정에서 str2에 대해 delete를 자동으로 호출해주지 않기 때문입니다. 그런데 str1은 제대로 해제되는데, 이는 스택에 있는 로컬 변수이기 때문입니다. 위에서 말했듯이 스택 풀기 과정에서 로컬에 있는 변수는 모두 제대로 해제됩니다.

 

바로 이 때문에 C 언어에서 사용하던 할당 모델과 익셉션 같은 최신 프로그래밍 기법을 섞어 쓰면 안됩니다. C 방식에서 new를 호출해서 C++처럼 보이게 만들어도 마찬가지 입니다. C++로 코드를 작성할 때는 반드시 스택 기반 할당 방식을 사용해야 합니다. 그게 힘들다면 아래에 나온 기법 중 하나를 활용합니다.

 

5.1 Using Smart Pointers

스택 기반 할당 기법을 사용할 수 없다면 스마트 포인터를 활용합니다. 그러면 익셉션 처리 과정에 메모리나 리소스 누수 방지 작업을 자동으로 처리할 수 있습니다. 스마트 포인터 객체가 제거될 때마다 그 포인터에 할당된 리소스도 해제됩니다. 예를 들어 앞에서 본 funcOne() 함수에서 unique_ptr을 사용하도록 수정해보겠습니다.

void funcOne()
{
    std::string str1;
    auto str2{ std::make_unique<std::string>("hello") };
    funcTwo();
}

여기서 str2 포인터는 funcOne()을 호출한 후, 리턴될 때 또는 그 안에서 익셉션이 발생할 때 자동으로 해제됩니다.

 

물론 특별한 이유가 있을 때만 동적으로 할당해야 합니다. 예를 들어 앞에 나온 funcOne에서 굳이 str2를 동적으로 할당할 필요가 없습니다. 스택 기반 string 변수로도 충분합니다.

 

5.2 Catch, Cleanup, and Rethrow

메모리 및 리소스 누수를 방지하지 위한 또 다른 방법은 각 함수마다 발생 가능한 익셉션을 모두 잡아서 리소스를 제대로 정리한 뒤 그 익셉션을 다시 스택의 상위 핸들러로 던지는 것입니다.

예를 들어 funcOne()을 이렇게 처리하도록 수정하면 다음과 같습니다.

void funcOne()
{
    std::string str1;
    std::string* str2{ new std::string{} };
    try {
        funcTwo();
    }
    catch (...) {
        delete str2;
        throw;
    }
    delete str2;
}

이 함수는 funcTwo()를 호출하는 문장과 여기에서 발생하는 익셉션을 처리하는 핸들러를 정의하고 있습니다. 이 핸들러는 리소스를 정리한 뒤 잡은 익셉션을 다시 던집니다. 이렇게 현재 잡은 익셉션을 다시 던질 때는 이전 포스팅에서 언급했듯이 throw 키워드만 적어줘도 됩니다.

 

위 코드는 조금 지저분해졌지만 익셉션을 모두 처리하는 데는 문제가 없습니다. 여기서 str2에 대해 delete를 호출하는 똑같은 문장이 두 번 나옵니다. 하나는 익셉션을 처리하는 catch문에서 실행하고 다른 하나는 함수가 정상적으로 종료할 때 호출합니다.

 


6. Common Error-Handling Issues

프로그램에서 익셉션 메커니즘을 사용할 지는 전적으로 개발자에게 달렸지만, 팀 단위로 작업할 때는 각자 알아서 처리하기보다는 프로젝트 차원에서 에러 처리 방식을 공식적으로 정해 두는 것이 좋습니다. 익셉션을 사용하면 에러 처리 방식을 일관성 있게 정리할 수 있어서 에러 처리가 쉬워집니다. 물론 익셉션을 사용하지 않아도 그렇게 할 수는 있습니다. 이때 중요한 점은 프로그램 전반에 통일된 에러 처리 기법을 적용하는 것입니다. 프로젝트에 참여한 모든 프로그래머는 이렇게 정해둔 에러 처리 규칙을 제대로 이해해서 똑같이 따라야 합니다.

 

아래에서 익셉션 메커니즘을 이용해서 에러를 처리할 때 흔히 발생하는 문제에 대해서 소개하도록 하겠습니다.

 

6.1 Memory Allocation Errors

이전 포스팅이나 위에서 소개한 예제들은 모두 메모리 할당 에러가 발생하지 않는다고 가정했습니다. 현재 흔히 사용하는 64비트 플랫폼에서 이런 일이 발생할 일은 거의 없지만, 모바일 시스템이나 레거시 시스템에서는 메모리 할당 에러가 드물지 않게 발생합니다. 이런 시스템에서는 반드시 메모리 할당 에러에 대처하는 코드를 구현해야 합니다. C++은 메모리 할당 에러를 처리하기 위한 다양한 기능을 제공합니다.

 

new나 new[]에서 메모리를 할당할 수 없을 때 기본적으로 수행하는 동작은 <new> 헤더 파일에 정의된 bad_alloc 익셉션을 던지는 것입니다. 따라서 이 익셉션을 처리하는 catch 구문을 작성합니다.

 

new나 new[]를 호출할 때마다 try/catch문으로 감싸도 되지만, 할당하려는 메모리 블록의 크기가 클 때만 이렇게 하는 것이 좋습니다. 메모리 할당 익셉션을 잡는 방법은 다음과 같습니다.

int* ptr = nullptr;
size_t integerCount = std::numeric_limits<size_t>::max();
try {
    ptr = new int[integerCount];
}
catch (const std::bad_alloc& e) {
    std::cerr << __FILE__ << "(" << __LINE__
        << "): Unable to allocate memory: " << e.what() << std::endl;
    return;
}
// ...

위 코드는 미리 정의된 전치리 매크로인 __FILE__과 __LINE__을 사용하고 있습니다.

 

물론 구현하는 프로그램에 따라 실행 과정에서 발생할 수 있는 모든 에러를 프로그램의 최상위 코드에서 try/catch 블록 하나만으로 처리해도 됩니다. 한 가지 주의할 점은 에러 로깅 과정에 메모리 할당이 발생할 수 있는데, new 과정에 에러가 발생했다면 에러 메세지를 로깅하는 데 필요한 메모리도 없을 가능성이 높습니다.

 

6.1.1 Non-throwing new

익셉션 메커니즘을 사용하지 않고, 예전 C 방식처럼 메모리 할당에 실패하면 널 포인터를 리턴하도록 작성해도 됩니다. C++은 익셉션을 던지지 않는 nothrow 버전의 new와 new[]도 제공합니다. 이 버전은 메모리 할당에 실패하면 익셉션을 던지지 않고 nullptr을 리턴합니다. 이렇게 하려면 new 대신 new(nothrow) 구문을 사용합니다. 예를 들면 다음과 같습니다.

size_t integerCount = std::numeric_limits<size_t>::max();
int* ptr = new(std::nothrow) int[integerCount];
if (ptr == nullptr) {
    std::cerr << __FILE__ << "(" << __LINE__
        << "): Unable to allocate memory!\n";
    return;
}
// ...

 

6.1.2 Customzing Memory Allocation Failure Behavior

C++은 new 핸들러 콜백 함수를 커스터마이즈하는 기능을 제공합니다. 기본적으로 new나 new[]는 new 핸들러를 따로 사용하지 않고 bad_alloc 익셉션을 던지기만 합니다. 그런데 new 핸들러를 정의하면 메모리 할당 루틴에서 에러가 발생했을 때 익셉션을 던지지 않고 정의된 new 핸들러를 호출합니다. new 핸들러가 리턴하면 메모리 할당 루틴은 메모리를 다시 할당하려 시도하는데, 이때 실패해도 다시 new 핸들러를 호출합니다. 따라서 new 핸들러에서 다음 3가지 중 한 가지 방식으로 구현하지 않으면 무한 루프가 발생할 수 있습니다.

  • 메모리 추가: 공간을 확보하기 위한 한 가지 방법은 프로그램 구동시 큰 덩어리의 메모리를 할당했다가 new 핸들러로 해제하게 만드는 것입니다. 구체적인 활용 예로 메모리 할당 에러가 발생할 때 현재 사용자의 상태가 사라지지 않도록 저장해야 할 때가 있습니다. 여기서 핵심은 프로그램을 구동할 때 원하는 상태(ex, 워프프로세서라면 문서 전체)를 저장할 수 있을 정도로 충분한 양의 메모리를 할당하는 데 있습니다. new 핸들러가 호출되면 이 블록을 해제한 뒤 상태(ex, 문서)를 저장하고 프로그램을 다시 구동해서 저장된 상태를 불러오면 됩니다.
  • 익셉션 던지기: C++ 표준에서는 new 핸들러에서 익셉션을 던질 때 반드시 bad_alloc이나 이를 상속한 익셉션을 던지도록 명시하고 있습니다. 
  • 다른 new 핸들러 설정: 이론적으로 new 핸들러를 여러 개 만들어서 각각 메모리를 생성하고 문제가 발생하면 다른 new 핸들러를 설정할 수 있습니다. 하지만 실제 효과에 비해 코드가 복잡하다는 단점이 있습니다.

new 핸들러에서 위 3가지 작업 중 어느 하나라도 하지 않으면 메모리 할당 에러가 발생할 때 무한 루프에 빠집니다.

 

메모리 할당 에러가 발생할 때 new 핸들러를 호출하지 않게 하고 싶다면 new를 호출하기 전에 new 핸들러의 디폴트값인 nullptr로 잠시 되돌려둡니다.

new 핸들러는 <new> 헤더 파일에 선언된 set_new_handler()를 호출해서 설정합니다. 예를 들어 에러 메세지를 로그에 기록하고 익셉션을 던지도록 new 핸들러를 작성하면 다음과 같습니다.

class please_terninate_me : public std::bad_alloc {};
void myNewHandler()
{
    std::cerr << "Unable to allocate memory.\n";
    throw please_terninate_me{};
}

int main()
{
    try {
        // Set the new new_handler and save the old one.
        std::new_handler oldHandler{ std::set_new_handler(myNewHandler) };

        // Generate allocation error.
        size_t numInt{ std::numeric_limits<size_t>::max() };
        int* ptr{ new int[numInt] };
        // Reset the old new_handler
        std::set_new_handler(oldHandler);
    }
    catch (const please_terninate_me&) {
        auto location{ std::source_location::current() };
        std::cerr << location.file_name() << "(" << location.line() << "): Terminating program.\n";
        return 1;
    }
}

여기서 new 핸들러는 함수 포인터 타입에 대한 typedef이며, set_new_handler()는 이를 인수로 받습니다.

 

6.2 Errors in Constructors

익셉션을 처음 접하면 생성자에서 발생하는 에러를 어떻게 처리할 지 막막할 수 있습니다. 생성자에서 객체를 생성하는 과정에 에러가 발생하면 어떻게 해야 할까요? 생성자는 원래 리턴값이 없기 때문에 익셉션 메커니즘이 없던 시절에 사용하던 에러 처리 메커니즘을 적용할 수 없습니다. 익셉션을 사용하지 않고서 할 수 있는 최선의 방법은 객체의 정상 생성 여부를 표시하는 플래그를 객체 안에 설정하는 것입니다. 그리고 그 플래그값을 리턴하는 메소드를 checkConstructionStatus()와 같은 이름으로 제공해서 클라이언트가 이 메소드로 생성자의 처리 상태를 확인하는 것입니다.

 

하지만 익셉션 메커니즘을 활용하면 이보다 훨씬 잘 처리할 수 있습니다. 생성자가 값을 리턴하지 못해도 익셉션을 던질 수는 있기 때문입니다. 익셉션을 활용하면 클라이언트가 객체의 정상 생성 여부를 쉽게 알 수 있습니다. 하지만 한 가지 심각한 문제가 있습니다. 익셉션이 발생해서 생성자가 정상 종료되지 않고 중간에 실행을 멈추고 빠져나와버리면 나중에 그 객체의 소멸자가 호출될 수 없습니다. 따라서 익셉션이 발생해서 생성자를 빠져나올 때는 반드시 생성자에서 할당했던 메모리와 리소스를 정리해주어야 합니다. 생성자가 아닌 일반 함수도 이 점에 주의해야 하는 것은 마찬가지지만 생성자에 대해서는 할당했던 메모리나 리소스를 소멸자가 알아서 해제해준다고 생각하기 쉽기 때문에 더욱 주의해야 합니다.

 

예제 코드를 살펴보겠습니다. 여기서는 생성자에서 할당한 리소스를 일반 포인터로 표현하는데 문제를 보여주기 위해서 이렇게 한 것이고, 실제로는 일반 포인터가 아닌 표준 라이브러리에서 제공하는 컨테이너와 같은 안전한 방법으로 구현해야 합니다.

template<typename T>
class Matrix
{
public:
    Matrix(size_t width, size_t height);
    virtual ~Matrix();
private:
    void cleanup();

    size_t m_width{ 0 };
    size_t m_height{ 0 };
    T** m_matrix{ nullptr };
};

Matrix의 구현 코드는 다음과 같습니다. 

template<typename T>
Matrix<T>::Matrix(size_t width, size_t height)
{
    m_matrix = new T*[width] {}; // Array is zero-initialized

    // Don't initialize the m_width and m_height members in the ctor-
    // initializer. These should only be initialized when the above
    // m_matrix allocation succeeds!
    m_width = width;
    m_height = height;

    try {
        for (size_t i = 0; i < width; ++i) {
            m_matrix[i] = new T[height];
        }
    }
    catch (...) {
        std::cerr << "Exception caught in constructor, cleaning up...\n";
        cleanup();
        // Nest any caught exception inside a bad_alloc exception
        std::throw_with_nested(std::bad_alloc{});
    }
}

template<typename T>
Matrix<T>::~Matrix()
{
    cleanup();
}

template<typename T>
void Matrix<T>::cleanup()
{
    for (size_t i = 0; i < m_width; i++) {
        delete[] m_matrix[i];
    }
    delete[] m_matrix;
    m_matrix = nullptr;
    m_width = m_height = 0;
}

여기서 첫 번째 new 호출은 try/catch 구문으로 묶여 있지 않습니다. 이 생성자는 나중에 해제할 리소스를 할당하지 않기 때문에 익셉션이 발생해도 문제가 없습니다. 하지만 그 뒤에 나오는 new 호출문이 익셉션을 던지면, 생성자에서 할당된 메모리가 있기 때문에 익셉션이 발생할 때 반드시 메모리를 해제해야 합니다. 하지만 T의 생성자에서 발생할 익셉션이 무엇인지 모르기 때문에 catch문이 모든 익셉션을 잡도록 ...으로 지정했습니다. 참고로 첫 번째 new 호출로 할당된 배열은 {} 구문을 이용하여 0으로 초기화헀습니다. 즉, 모든 원소가 nullptr이라는 값을 가집니다. 이렇게 하면 cleanup() 메소드에서 처리하기가 편한데, nullptr에 대해 delete를 호출할 수 있기 때문입니다.

 

여기서 상속을 적용하면 어떻게 될까요?

파생 클래스의 생성자보다 베이스 클래스의 생성자가 먼저 실행되기 때문에 파생 클래스 생성자에서 익셉션이 발생하면 C++ 런타임은 생성자를 정상적으로 실행했던 베이스 클래스의 소멸자를 호출하게 됩니다.

C++은 생성자 실행을 정상적으로 마친 객체에 대해 소멸자가 실행되도록 보장합니다. 따라서 생성자에서 익셉션 없이 정상 처리된 객체는 소멸자가 반드시 호출됩니다.

 

6.3 Function-Try-Blocks for Constructors

지금까지 소개한 익셉션 기능만으로도 함수에서 발생한 익셉션을 처리하는 데 충분합니다. 그렇다면 생성자 이니셜라이저(ctor-initializer)에서 발생한 익셉션은 어떻게 처리해야 할까요? 여기서는 함수 try 블록(function-try-blocks)이란 기능으로 이런 익셉션을 처리하는 방법을 소개합니다. 함수 try 블록은 일반 함수뿐만 아니라 생성자에 적용할 수도 있습니다. 이 기능은 추가된지 상당히 오래됬음에도 불구하고 잘 모르는 경우가 많습니다.

 

생성자에 대한 함수 try 블록을 작성하는 방법을 의사코드로 표현하면 다음과 같습니다.

MyClass::MyClass()
try
    : <ctor-initializer>
{
    /* .. constructor body .. */
}
catch (const std::exception& e)
{
    /* ... */
}

여기서 try 키워드를 생성자 이니셜라이저의 바로 앞에 적었습니다. catch 문은 반드시 생성자를 닫는 중괄호 뒤에 나와야 합니다. 그러므로 실질적으로는 생성자 바깥에 놓이게 됩니다. 함수 try 블록을 생성자에 적용할 때는 다음과 같은 주의사항이 있습니다.

  • catch문은 생성자 이니셜라이저나 생성자 본분에서 발생한 익셉션을 잡아서 처리한다.
  • catch문은 반드시 현재 발생한 익셉션을 다시 던지거나 새 익셉션을 만들어 던져야 한다. catch문에서 이렇게 처리하지 않으면 런타임에서 자동으로 현재 익셉션을 다시 던진다.
  • catch문은 생성자에 전달된 인수에 접근할 수 있다.
  • catch문이 함수 try 블록에서 익셉션을 잡으면 생성자의 실행을 정상적으로 마친 베이스 클래스나 그 객체로 된 멤버는 catch문이 시작하기 전에 소멸된다.
  • catch문 안에서는 객체로 된 멤버 변수에 접근하면 안된다. 바로 위에서 설명한 것처럼 catch문이 실행되기 전에 소멸되기 때문이다. 그런데 익셉션이 발생하지 전에 그 객체에서 non-class 타입 데이터 멤버를 초기화했다면 여기서 접근할 수 있다. 단, 이런 리소스를 정리하는 작업은 catch문에서 처리해야 한다.
  • 함수 try 블록에 있는 catch문은 그 안에 담긴 함수에서 값을 리턴할 때 return 키워드를 사용할 수 없다. 생성자는 원래 아무것도 리턴하지 않기 때문이다.

위에서 나열한 제약사항을 감안하면 생성자에 대한 함수 try 블록은 다음과 같은 제한된 상황에만 적합합니다.

  • 생성자 이니셜라이저에서 던진 익셉션을 다른 익셉션으로 변환할 때
  • 메세지를 로그 파일에 기록할 때
  • 생성자 이니셜라이저에서 할당했지만 소멸자로 자동 제거할 수 없는 리소스를 익셉션을 던지기 전에 해제할 때

 

다음 예는 함수 try 블록을 구현하는 방법을 보여줍니다. 여기서 SubObject 클래스는 runtime_error 익셉션을 던지는 생성자 하나만 가지고 있습니다.

class SubObject
{
public:
    SubObject(int i) { throw std::runtime_error{ "Exception by SubObject ctor" }; }
};

다음은 MyClass 클래스입니다. 이 클래스는 int* 타입의 멤버 변수 하나와 SubObject 타입의 멤버 변수 하나를 갖고 있습니다.

class MyClass
{
public:
    MyClass();
private:
    int* m_data{ nullptr };
    SubObject m_subObject;
};

SubObject 클래스에는 디폴트 생성자가 없습니다. 다시 말해 m_subObject를 MyClass의 생성자 이니셜라이저로 초기화해야 합니다. MyClass는 함수 try 블록을 이용하여 생성자 이니셜라이저에서 발생한 익셉션을 처리합니다.

MyClass::MyClass()
try
    : m_data{ new int[42]{1,2,3} }, m_subObject{ 42 }
{
    /* ... constructor body ...*/
}
catch (const std::exception& e)
{
    // Cleanup memory
    delete[] m_data;
    m_data = nullptr;
    std::cout << "function-try-block caught: '" << e.what() << "'\n";
}

여기서 명심할 점은 생성자에 대한 함수 try 블록 안에 있는 catch문은 반드시 현재 익셉션을 다시 던지거나 새 익셉션을 생성해서 던져야 합니다. 위의 catch문을 보면 아무 익셉션도 던지지 않는데, 그러면 C++ 런타임에서 현재 익셉션을 대신 던져줍니다. 위에서 정의한 클래스를 사용하는 예는 다음과 같습니다.

int main()
{
    try {
        MyClass m;
    }
    catch (const std::exception& e) {
        std::cout << "main() caught: '" << e.what() << "'\n";
    }
    return 0;
}

참고로 이 예제 코드처럼 작성하면 위험합니다. 초기화 순서에 따라 catch문에 진입할 때 m_data에 이상한 값이 할당될 수 있습니다. 이렇게 알 수 없는 값을 가진 포인터에 대해 delete를 호출하면 예상치 못한 동작이 발생할 수 있습니다. 그래서 위 예제처럼 이런 문제를 방지하려면 m_data 멤버를 std::unique_ptr과 같은 스마트 포인터로 선언하고, 함수 try 블록을 제거하는 것이 좋습니다.

함수 try 블록은 사용하지 않는 것이 좋습니다. 이는 주로 일반 포인터 타입 리소스를 가진 데이터 멤버가 있을 때만 유용합니다. std::unique_ptr과 같은 RAII 클래스를 이용하면 리소스를 이렇게 표현하지 않게 할 수 있습니다.

함수 try 블록은 생성자뿐만 아니라 일반 함수에도 적용할 수 있습니다. 하지만 일반 함수에서 이를 사용해서 더 나아질 것이 없습니다. 단, 함수 try 블록을 생성자에서 사용할 때와 달리 일반 함수에서 사용하면 현재 익셉션을 다시 던지거나 catch 문에서 새 익셉션을 만들어서 던질 필요가 없고, C++ 런타임에서 대신 던져주지도 않습니다.

 

6.4 Errors in Destructors

소멸자에서 발생하는 에러는 소멸자에서 처리해야 합니다. 소멸자에서 익셉션을 다른 곳으로 던지면 안되는데 그 이유는 다음과 같습니다.

  • 소멸자를 명시적으로 noexcept(false)로 지정하지 않거나, 그 클래스에 있는 객체 중 소멸자에 noexcept(false)가 지정된 것이 없다면 내부적으로 noexcept로 선언된 것으로 취급한다. noexcept 소멸자에서 익셉션을 던지면 C++ 런타임은 std::terminate()를 호출해서 프로그램을 종료한다.
  • 소멸자는 이미 다른 익셉션이 발생해서 스택 풀기를 수행하는 과정에서도 실행될 수 있다. 스택 풀기를 하는 도중에 소멸자에서 익셉션을 던지면 C++ 런타임은 std::terminate()를 호출해서 프로그램을 종료한다. 
    부연 설명을 하자면, C++은 소멸자가 호출되는 원인이 일반 함수의 정상적인 종료 때문인지 아니면 delete를 호출했기 때문인지 아니면 스택 풀기 때문인지 알아내는 기능을 제공한다. <exception> 헤더 파일에 선언된 uncaught_exceptions() 함수를 호출하면 아직 잡지 않은 익셉션, 즉, 이미 발생했지만 아직 catch문에 매칭되지 않은 익셉션의 수를 리턴한다. uncaught_exceptions()의 리턴값이 0보다 크면 스택 풀기 과정에 있다는 뜻이다. 하지만 이 함수를 제대로 활용하기 힘들고 코드도 지저분해져서 사용하지 않는 것이 좋다.
  • 클라언트는 소멸자를 직접 호출하지 않고 delete를 이용하여 간접적으로 소멸자를 호출하는데, 소멸자에서 익셉션을 던지면 클라이언트가 할 수 있는 일이 없기 때문에 굳이 클라이언트에게 익셉션 처리의 부담을 줄 필요가 없다.
  • 소멸자는 개체에서 사용할 메모리나 리소스를 해제할 마지막 기회이므로, 함수 실행 도중에 익셉션을 던져 이 기회를 놓쳐버리면 다시 돌아가 메모리나 리소스를 해제할 수 없다.

 


 

 

이상으로 C++에서 익셉션에 대한 내용들에 대해 알아봤습니다.. !

댓글