생성자( constructor )에서의 예외 처리
생성자를 작성하다 보면, 의외로 문제가 발생할 여지가 꽤 있다는 것을 깨닫게 됩니다.
맨 먼저 떠오르는 경우는, 메모리를 대규모로 할당할 때, 실패하는 경우를 들 수 있습니다.
또 다른 경우로는, 클래스 객체가 지속적으로 사용하게 될 변수의 값을 채워 넣기 위해, 생성자에서 호출하는 계산 함수의 오류를 들 수 있습니다.
하지만, 이런 문제들은 다른 곳에서도 얼마든지 일어날 수 있는 경우들입니다.
그러나, 일반 함수와 달리, 생성자는 성공 여부를 반환할 수 없기 때문에, 이런 경우를 자연스럽게 처리할 수 없습니다.
그래서 이를 우회하는 방식으로, 실패했다는 정보를 객체 안에 저장해 두거나, 객체를 사용 전 검산을 통해서 제대로 된 결과를 갖고 있다는 것을 확인해야 합니다.
이런 경우에 C++의 예외( exception ) 처리 메커니즘이 아주 적합한 처리 방법이라고 알 수 있습니다.
[C++] 예외 처리를 위한 throw와 try-catch의 동작 방식
C++에서의 예외( exception ) 처리C++에서는 프로그램의 오류나 기대하지 못했던 상황이 발생한 경우, 이를 처리하기 위한 예외( exception )를 발생시키면, 프로그램이 자동으로, 이 예외를 처리할 수
codingbonfire.tistory.com
아래의 예문의 CSomeObj
객체는, 생성자에서 메모리를 할당하고, 소멸자에서 할당했던 메모리를 자동으로 반환하는 객체입니다.
이 객체의 생성자에서 std::runtime_error
타입의 예외가 발생한다고 해봅시다.
( 참고로, runtime_error
는 C++ 표준 라이브러리가 지원하는 예외 클래스( exception class )로서, 사용자가 입력한 문자열을 멤버 함수 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
● 첫 번째, main 함수에서, pObj
포인터에 지정되었던, 할당된 메모리는, 컴파일러에 의해 자동으로 삭제된다는 것입니다.
CSomeObj* pObj = new CSomeObj();
위와 같은 문장을 수행하면, CSomeObj
를 위한 메모리를 할당하고, 생성자를 호출합니다.
그런데, 이 클래스의 생성자에서 예외가 발생했으므로, 이미 CSomeObj
객체를 위해 할당되었던 메모리를 삭제하고, 그 즉시 try 블록을 벗어나서 catch 블록으로 실행 제어( execution control )를 이동시킵니다.
따라서, try 블록에 있는, 아래 문장들은 실행되지 않습니다.
delete pObj;
pObj = nullptr;
● 두 번째, 위 결과를 보면 알 수 있듯이, CSomeObj
의 소멸자가 호출되지 않는다는 것입니다.
생성자에서 예외가 발생했으므로, 이 함수( 생성자 )는 실행을 바로 멈추고, main 함수의 catch 블록에서 예외 처리를 하게 됩니다.
그래서, CSomeObj
객체는 생성되지 않았고, 따라서 소멸자도 호출되지 않습니다.
그 결과, CSomeObj
생성자에서 m_pData
에 할당된 메모리는 시스템에 반환되지 않습니다.
그럼, 이 문제를 어떻게 처리해야 할까요?
생성자 내에서의 예외( exception ) 처리
먼저 생각해 볼 방법으로, 생성자 내에서 예외가 발생 시, 일단 이 예외 처리를 한 후에, 생성자를 호출한 함수로 다시 예외를 던지는 방법입니다.
이것을 코드로 작성하면 다음과 같습니다.
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
에 할당된 메모리를 해제하는 예외 처리를 하고, 다시 함수 밖으로 예외를 전달하는 방법이죠.
그렇지만, 위에 보다시피, 긴 코드를 작성해야 하고, main 함수 내에도, CSomeObj
의 생성자와 같이, runtime_error
타입의 예외를 처리하는 코드를 다시 작성해야 합니다.
게다가, 다른 타입의 예외들이 발생할 가능성이 있는 경우, 이를 해결하기 위한 처리기들에도 위와 같은 코드를 다시 작성해줘야 합니다.
자원을 관리하는 객체 멤버 사용
위의 문제를 해결하는 또 다른 방법으로, 자원을 관리하는 객체를 클래스 멤버로 사용하는 방법이 있습니다.
아래의 CBuffer
클래스는, CSomeObj
클래스의 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
생성자에서 예외가 발생하면 실행 제어가 main 함수의 catch 블록으로 건너뛰기 전에, CSomeObj
객체를 위한 메모리를 삭제한다고 했었습니다.
그리고, 이 메모리를 삭제하기 전에, 이미 생성되었던 클래스 데이터 멤버들도 모두 파괴합니다.
그 과정에서, 클래스 멤버인 m_Data
객체의 소멸자( destructor )이 호출되고, 이때 이 객체가 관리하고 있던 메모리도 해제됩니다.
이 방식은, 이전의 방식보다는, 좀 더 만족스러운 처리 방법이라고 할 수 있을 것 같습니다.
하지만, 메모리 자원이 필요할 때마다, 이를 위해서 CBuffer
같은 클래스를 작성하는 것은 비효율적입니다.
다행히, 이러한 경우 사용할 만한 C++ 표준 라이브러리의 클래스가 있습니다.
바로, 메모리 자원을 자동으로 관리하는 std::unique_ptr입니다.
[C++] 독점적으로 자원을 관리하는 std::unique_ptr 객체
std::unique_ptrstd::unique_ptr은, C++ 11에, 할당된 메모리를 편리하게 관리할 목적으로 도입된 스마트 포인터( smart pointer ) 클래스입니다. 이 unique_ptr 객체가 하는 일은, 메모리가 할당된 객체의 주소를
codingbonfire.tistory.com
이 클래스를 사용해서 위의 코드를 변경하면 다음과 같습니다.
#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";
}
};
이전 CBuffer
클래스 객체처럼, 이 unique_ptr
객체도 파괴될 때, 관리하고 있던 메모리를 자동으로 시스템에 반환합니다.
그리고, main 함수에도 이 클래스를 사용하여 구조적으로 간단하고, 읽기 쉬운 코드를 작성할 수 있습니다.
int main(){
try{
unique_ptr<CSomeObj> pObj = make_unique<CSomeObj>();
// do something...
}
catch(std::runtime_error e){
cout << e.what();
}
return 0;
}
위의 make_unique<CSomeObj>
함수는, 메모리에 할당된 CSomeObj
객체를 관리하는 unique_ptr
객체를 생성합니다.
그리고, 정상적인 실행 제어가 try 블록을 벗어나는 순간, unique_ptr
객체인 pObj
는 삭제되고, pObj
이 관리하고 있던 메모리도 같이 해제되어 시스템으로 반환됩니다.
소멸자( destructor )에서 예외 처리
소멸자에서 예외가 발생하면, 소멸자 내부에서 발생한 예외를 처리해야 합니다.
왜냐하면, 클래스의 소멸자는 암시적으로 noexcept 속성이 있기 때문입니다.
[C++] 예외가 발생되지 않는 함수를 선언하는 noexcept
noexcept 지시자( specifier )noexcept는 함수 이름의 끝에 붙는 지시자로, 이 지시자가 붙은 함수는 예외( exception )를 발생시키지 않는다는 것을 명시합니다. 하지만, noexcept 함수 실행 시, 컴파일러에
codingbonfire.tistory.com
이 noexcept 함수는 예외( exception )를 발생하지 않는다라고 약속하는 함수로, 만약 이 함수에서 발생된 예외가 외부로 나가게 되면, std::terminate 함수가 실행되어, 바로 프로그램이 종료됩니다.
다음은 소멸자에서 예외( exception )를 발생시키는 예문입니다.
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(std::runtime_error e){
cout << e.what();
}
return 0;
}
▼출력
CSomeObj destructor called
terminate called after throwing an instance of 'std::runtime_error'
what(): Exception was occured in destructor
main 함수에서 CSomeObj
객체가 try 블록을 벗어나면서 파괴될 때, CSomeObj
클래스의 소멸자( destructor )에서 예외를 발생시키게 됩니다.
그런데, 이 main 함수에는 catch-all 처리문이 있기 때문에, 예외가 처리될 것이라고 생각할 수 있지만, 생성된 예외가 소멸자를 벗어나는 순간 프로그램은 종료됩니다.
그럼 만약, 소멸자에 noexcept 속성이 없다면 어떻게 될까요?
그래도, 마찬가지로 프로그램은 종료될 것입니다.
( 참고로, 함수가 예외를 발생시킬 수 있다고 표시할 때는, 함수 뒤에 noexcept(false) 키워드를 사용해야 합니다. )
class CSomeObj{
public:
void throw_exception_func(){
std::runtime_error e("Exception was occured in a function\n");
throw e;
}
~CSomeObj() noexcept(false) {
cout << "CSomeObj destructor called\n";
std::runtime_error e("Exception was occured in destructor\n");
throw e;
}
};
int main(){
try{
CSomeObj obj;
obj.throw_exception_func();
}
catch(std::runtime_error e){
cout << e.what();
}
return 0;
}
위와 같이 obj
객체의 throw_execption_func() 멤버 함수를 호출하면, 예외가 발생하는데, 이렇게 되면 스택 언와인딩( stack unwinding )을 통해서 obj
객체를 삭제하게 됩니다.
그러면, obj
객체의 소멸자를 호출하게 되고, 다시 또 다른 예외가 발생되는데, 컴파일러는 이미 예외를 처리하고 있는 중이기 때문에, 이 예외를 처리할 수 없어서 프로그램이 종료되는 것입니다.
그래서, 클래스의 소멸자가 암시적으로 noexcept 속성을 갖고 있도록 만든 것입니다.
그리고, 어쩔 수 없이 소멸자에서 예외의 발생이 필요한 경우, 이 소멸자 내부에서 예외를 처리하고, 이를 외부로 전파시키면 안 됩니다.
정리
- 클래스 객체의 생성 여부를 알리기 위해, 생성자 내부에서 예외( exception )를 발생시키는 것은 편리합니다.
- 클래스 소멸자는 암시적으로 noexcept 함수입니다.
- 클래스의 소멸자에서 예외를 외부로 전파시키는 것은 위험합니다.
이 글과 관련 있는 글들
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 예외 클래스( exception class ) (0) | 2025.04.04 |
---|---|
[C++] 예외가 발생되지 않는 함수를 선언하는 noexcept (0) | 2025.04.03 |
[C++] 예외 처리를 위한 throw와 try-catch의 동작 방식 (1) | 2025.04.01 |
[C++] 불필요한 중간 단계를 건너뛰기 위한 복사 생략( copy elision ) (0) | 2025.03.31 |
[C++] 기존의 객체를 복사할 때 호출되는 복사 생성자( copy constructor ) (0) | 2025.03.30 |