C++에서 예외 처리를 할 때 catch 블록이 어떻게 동작하는지, 그리고 예외 객체의 타입 변환(type conversion)이 어떤 규칙을 따르는지 알아보겠습니다. 많은 개발자들이 착각하기 쉬운 부분이 바로 이 타입 변환인데, catch 블록은 일반적인 함수 호출과 달리 암시적 타입 변환이 적용되지 않는다는 점이 핵심입니다.
첫 번째 예제: char 타입 예외와 int catch 블록
먼저 간단한 코드를 살펴보고, 어떤 결과가 출력되는지 그 이유를 분석해 보겠습니다.
#include <iostream>
using namespace std;
int main() {
try{
throw 'a';
}
catch(int a) {
cout << "Integer value is caught :" << a;
}
catch(...) {
cout << "Entering into default catch block";
}
}실행 결과
Entering into default catch block
출력 결과를 보면 의아할 수 있습니다. 문자 'a'를 던졌는데, 첫 번째 catch 블록은 int 타입을 받도록 작성되어 있습니다. 문자 'a'의 ASCII 코드 값은 정수이므로 첫 번째 블록에 걸릴 것이라고 생각하기 쉽습니다.
하지만 실제로는 기본 catch 블록인 catch(...)가 실행됩니다. 그 이유는 catch 블록에서는 char에서 int로의 암시적 타입 변환이 일어나지 않기 때문입니다. 던져진 예외의 타입은 정확히 char이며, 컴파일러는 이를 int로 변환하지 않고 정확한 타입 매칭 또는 ...(모든 타입 허용)만으로 예외를 잡습니다.
두 번째 예제: 변환 생성자는 호출되지 않는다
이번에는 클래스 타입의 예외 객체를 다뤄보겠습니다. 이 예제에서는 변환 생성자(conversion constructor)가 던져진 객체에 대해 호출되지 않는다는 점을 확인할 수 있습니다.
#include <iostream>
using namespace std;
class TestExcept1 {};
class TestExcept2 {
public:
TestExcept2 (const TestExcept1 &e ){ // 변환 생성자 정의
cout << "From the Conversion constructor";
}
};
main() {
try{
TestExcept1 exp1;
throw exp1;
} catch(TestExcept2 e2) {
cout << "Caught TestExcept2 " << endl;
} catch(...) {
cout << "Entering into default catch block " << endl;
}
}실행 결과
Entering into default catch block
TestExcept1 타입의 객체를 던졌지만, TestExcept2로 변환해 주는 생성자가 존재함에도 불구하고 해당 catch 블록은 실행되지 않았습니다. 일반적인 상황이라면 TestExcept1 객체를 받아 TestExcept2 객체를 만드는 변환 생성자가 호출될 수 있지만, 예외 처리 메커니즘에서는 이러한 사용자 정의 변환 생성자가 호출되지 않습니다.
정리: catch 블록의 타입 매칭 규칙
지금까지의 내용을 종합하면 다음과 같은 핵심 원칙을 도출할 수 있습니다.
- catch 블록은 던져진 예외와 정확히 같은 타입일 때만 매칭됩니다.
- char → int 같은 기본 타입 간 암시적 변환은 catch 블록에서 적용되지 않습니다.
- 변환 생성자를 통한 사용자 정의 타입 변환 역시 호출되지 않습니다.
- 파생 클래스(derived class) 객체가 던져질 때도 기반 클래스(base class) 타입으로 자동 변환되지 않으므로, 기반 클래스 타입의 catch 블록으로는 파생 클래스 예외를 잡을 수 없습니다. 단, 포인터나 참조 형태로 던지는 경우에는 다형성에 의한 매칭이 가능합니다.
따라서 예외 처리 코드를 작성할 때는 던져지는 예외의 정확한 타입에 맞춰 catch 블록을 설계하거나, 모든 타입을 처리할 수 있는 catch(...)를 마지막 안전망으로 활용하는 것이 좋습니다.