C, C++/C++ 언어 / / 2024. 8. 24.

[C++] 생성자와 소멸자에서의 예외( exception ) 처리

생성자( constructor )에서의 예외 처리

생성자를 작성하다 보면, 의외로 문제가 발생할 여지가 꽤 있다는 것을 깨닫게 됩니다.

맨 먼저 떠오르는 경우는, 메모리를 대규모로 할당하는 경우를 들 수 있습니다.

그리고, 객체가 지속적으로 사용하게 될 변수의 값을 채워 넣기 위해, 생성자에서 호출하는 계산 함수의 오류를 떠올릴 수 있습니다.

 

하지만, 이런 문제들은 다른 곳에서도 얼마든지 일어날 수 있는 경우들입니다.

그러나, 생성자는 일반 함수처럼, 결과를 반환할 수 없기 때문에, 이런 경우를 자연스럽게 처리할 수 없습니다.

이를 처리하려면, 실패했다는 정보를 객체 안에 저장해 두거나, 검산을 통해서 제대로 된 결과를 갖고 있다는 것을 확인해야 합니다.

 

이런 경우에 C++의 예외 처리 메커니즘이 아주 적합한 처리 방법이라고 알 수 있습니다.

하지만, 이를 제대로 사용하기 위해선, 예외가 발생했을 때 발생하는 일들에 대해 알고 있어야 합니다.

여기에서 예외 처리에 관한 기본적인 내용을 볼 수가 있습니다.

 

[C++] 오류 처리를 위한 throw와 try, catch의 동작 방식

C++에서의 예외 처리C++에서는 프로그램의 오류나 기대하지 못했던 상황이 발생한 경우, 이를 처리하기 위해 예외( exception )를 발생시키면, 프로그램이 자동으로, 이 예외를 처리할 수 있는 코드

codingembers.co.kr

 

아래의 예문의 CSomeObj 객체는 생성자에서 자원을 할당하고, 소멸자에서 자동으로 자원을 반환하는 객체입니다.

이 객체의 생성자에서 std::runtime_error 타입의 예외가 발생한다고 해봅시다.

 

참고로, runtime_error는 사용자가 입력한 문자열을 멤버 함수 what을 통해 출력합니다.

여기선 설명하기 사용했습니다. 어떤 타입의 예외도 상관은 없습니다.

 

그럼, 이 예문에선 두 가지 문제가 발생합니다.

#include <exception>	// for runtime_error

#define SIZE    100

class CSomeObj{

    int* m_pData;
public:
    
    CSomeObj(){
        
        m_pData = new int[SIZE];    // 자원 할당
        
        // 예외 발생
        std::runtime_error e("Exception occurred in constructor");
        throw e;
    }

    ~CSomeObj(){
        
        cout << "CSomeObj destructor called\n";
        delete [] m_pData;
    }
};

int main(){

    try{
        CSomeObj* pObj = new CSomeObj();
        
        // do something...

        delete pObj;
        pObj = nullptr;
    }
    catch(std::runtime_error e){
        
        cout << e.what() << endl;	// 예외의 원인 출력
    }

    return 0;
}

▼출력

Exception occurred in constructor

 

첫 번째는 CSomeObj의 소멸자가 호출되지 않는다는 것입니다.

생성자에서 예외가 발생했으므로, 함수가 실행을 멈추고 바로 main 함수의 catch 블록에서 예외 처리를 하게 됩니다.

그러므로, CSomeObj 객체는 생성되지 않았고, 따라서 소멸자도 호출되지 않습니다.

그래서, CSomeObj 생성자에서 m_pData에 할당된 메모리는 시스템에 반환되지 않습니다.

 

두 번째는 main 함수에서 pObj에 할당된 메모리도 반환되지 않는다는 것입니다.

CSomeObj* pObj = new CSomeObj();

위와 같은 문장을 수행하면,  CSomeObj를 위한 메모리를 할당하고, 생성자를 호출합니다.

그런데, 생성자에서 예외가 발생했으므로, 그 즉시 try 블록을 벗어나서 catch 블록으로 실행 제어를 이동시킵니다.

따라서, 아래 문장들은 실행되지 않습니다.

delete pObj;
pObj = nullptr;

이렇게 해서, pObj에 할당된 메모리도 역시 반환되지 않습니다.

 

더 안 좋은 소식은 try 블록을 벗어나면서, 포인터인 pObj 자체가 파괴되므로, pObj에 할당되어 있는 메모리를 반환할 방법이 완전히 없어졌다는 것입니다.

 

이러한 문제점들을 어떻게 처리해야 할까요?

 

생성자 내에서 예외 처리

먼저 생각해 볼 방법으로, 생성자 내에서 예외가 발생 시, 일단의 예외 처리를 한 후에, 생성자를 호출한 함수로 다시 예외를 던지는 방법입니다.

 

이것을 코드로 작성하면 다음과 같습니다.

class CSomeObj{

    int* m_pData;
public:
    
    CSomeObj(){
                
        try{
            m_pData = new int[SIZE];    // 자원 할당
                    
            // 예외 발생
            std::runtime_error e("Exception was occured in constructor\n");
            throw e;
        }
        catch(const std::runtime_error& e){
            
            delete [] m_pData;  // 메모리 해제
            m_pData = nullptr;

            throw e;    // 다시 예외 발생
        }
    }

    ~CSomeObj(){
        
        cout << "CSomeObj destructor called\n";
        if ( m_pData )
            delete [] m_pData;
    }
};

CSomeObj의 생성자 안에서, m_pData에 할당된 메모리를 해제하는 예외 처리를 하고, 다시 함수 밖으로 예외를 전달하는 방법이죠.

이 방식은 m_pData의 메모리 문제를 해결할 수 있습니다.

그렇지만, 위에 보다시피, 긴 코드를 작성해야 하고, 여러 타입의 예외가 발생하는 경우, 이를 해결하기 모든 처리기에도 자원을 해제하는 코드를 다시 작성해야 합니다.

게다가, 이 방식은 main 함수의 메모리 문제는 해결할 수 없습니다.

 

자원을 관리하는 데이터 멤버 사용

위의 문제를 해결하는 또 다른 방법으로, 자원을 관리하는 객체를 사용하는 방법이 있습니다.

 

CBuffer 클래스는 m_pData 멤버 변수 대신 메모리를 자동으로 관리하는 객체입니다.

이 클래스는 할당된 메모리를 관리하고, 객체가 파괴될 때, 관리하는 메모리를 반환합니다.

class CBuffer{
    int* m_pData = nullptr;

public:

    void SetBuffer( int* pData){
        m_pData = pData;
    }

    ~CBuffer(){
        if ( m_pData)
            delete [] m_pData;
    }
};

class CSomeObj{

    CBuffer m_Data;	// 자원을 관리하는 클래스 멤버
public:
    
    CSomeObj(){
                
        m_Data.SetBuffer( new int[SIZE]);    // 자원 할당
                    
        // 예외 발생
        std::runtime_error e("Exception occurred in constructor\n");
        throw e;
    }

    ~CSomeObj(){
        cout << "CSomeObj destructor called\n";
    }
};

CSomeObj 생성자에서 예외가 발생하면 실행 제어가 catch 블록으로 건너뛰기 전에, 스택 언와인딩( stack unwinding )을 수행합니다.

그 과정에서, m_Data 객체가 파괴되고, 이 객체가 관리하고 있던 메모리도 자동으로 해제됩니다.

이렇게 해서, 만족할만하게 할당된 메모리 문제가 해결되었습니다.

 

그런데, 자원이 필요할 때마다, 이를 위해서 CBuffer 같은 클래스를 작성하는 것은 비효율적입니다.

다행히, 이러할 경우 사용할 만한 C++ 클래스가 있습니다.

바로, 메모리 자원을 자동으로 관리하는 unique_ptr입니다.

 

unique_ptr에 관한 내용은 여기서 볼 수 있습니다.

 

[C++] unique_ptr에 대한 설명과 사용법

unique_ptr 이란unique_ptr은 할당된 메모리를 자동으로 관리할 목적으로 C++ 11에 도입된 유틸리티 클래스입니다. 참고로, 이와 비슷한 스마트 포인터( smart pointer ) 클래스로 auto_ptr이 있었습니다.그러

codingembers.co.kr

 

이 클래스를 사용해서 위의 코드를 변경하면 다음과 같습니다.

#include <memory>	// for unique_ptr

class CSomeObj{

    unique_ptr<int[]> m_Data;
public:
    
    CSomeObj(){
                
        m_Data.reset(new int[SIZE]);  // 자원 할당
                            
        // 예외 발생
        std::runtime_error e("Exception occurred in constructor\n");
        throw e;
    }

    ~CSomeObj(){
        cout << "CSomeObj destructor called\n";
    }
};

 

그리고, 이 방식은 main 함수의 메모리 문제도 해결할 수 있습니다.

int main(){

    try{
        unique_ptr<CSomeObj> pObj = make_unique<CSomeObj>();
                
        // do something...
    }
    catch(std::runtime_error e){
        
        cout << e.what();
    }

    return 0;
}

실행 제어가 try 블록을 벗어나는 순간 uniqut_ptr 객체인 pObj는 삭제됩니다.

따라서, pObj이 관리하고 있던 메모리도 같이 해제되어 시스템으로 반환됩니다.

 

 

소멸자( destructor )에서 예외 처리

소멸자에서 예외가 발생하면, 소멸자 내부에서 발생한 예외를 처리해야 합니다.

왜냐하면, 클래스의 소멸자는 암시적으로 noexcept 속성이 있기 때문입니다.

 

noexcept 키워드에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++] noexcept 키워드를 사용하는 이유

noexcept 키워드noexcept는 함수 이름의 끝에 붙는 키워드로, 이 키워드가 붙은 함수는 예외를 발생시키지 않는다는 것을 명시합니다. 참고로, noexcept( true )로 쓸 수도 있는데, 이것은 noexcept와 같은

codingembers.co.kr

 

아래의 예문은 소멸자에서 예외를 발생시키는 예문입니다.

class CSomeObj{

public:
    
    ~CSomeObj(){
        cout << "CSomeObj destructor called\n";

        // 예외 발생
        std::runtime_error e("Exception occurred in destructor\n");
        throw e;
    }
};

int main(){

    try{
        CSomeObj obj;
    }
    catch(...){
        cout << "exception occurred\n";
    }

    return 0;
}

▼출력

CSomeObj destructor called
terminate called after throwing an instance of 'std::runtime_error'
  what():  Exception was occured in destructor

main 함수에서 CSomeObj가 파괴될 때, CSomeObj의 소멸자에서 예외를 발생시킵니다.

그런데, main 함수는 catch-all 처리문이 있기 때문에, 예외가 처리될 것이라고 생각할 수 있지만, 생성된 예외가 소멸자를 벗어나는 순간 프로그램은 종료됩니다.

 

만약, noexcept 속성이 없다면 어떻게 될까요?

그래도, 마찬가지로 프로그램은 종료될 것입니다.

왜냐하면, 소멸자 외부에 예외 처리를 할 수 있는 catch 블록이 있다고 하더라도, 그전에 스택 언와인딩( stack unwinding )을 거치게 됩니다.

이 과정에서, CSomeObj 객체를 다시 삭제합니다. 그러면, 다시 예외가 발생하고, 다시 객체를 삭제해야 하는 문제가 발생합니다.

그리고, 만약 예외 처리를 할 수 있는 catch 블록이 없다면, 당연히 std::terminate가 호출될 것입니다.

( 발생한 예외를 처리하지 못하면 프로그램은 종료됩니다. )

 

그래서, 클래스의 소멸자는 암시적으로 noexcept 속성을 갖고 있습니다.

그리고, CSomeObj의 소멸자에서 예외를 처리하고, 이를 외부로 전파시키면 안 됩니다.

class CSomeObj{

public:

    ~CSomeObj(){
        
        try{        
            // 예외 발생
            std::runtime_error e("Exception occurred in destructor\n");
            throw e;
        }
        catch(...){
            // 처리하는 내용이 없더라도, 내부에서 예외를 처리해야 합니다.
        }
        
        // 예외가 잘 처리되었으면, 이 문장이 출력될 것입니다.
        cout << "CSomeObj destructor called\n";
    }
};

▼출력

CSomeObj destructor called

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유