try-catch 구문
try에서 발생한 예외가 타입에 맞는 catch로 전달되는 과정을 익히고 여러 예외를 참조로 안전하게 처리합니다.
예외 처리의 기본 개념에서 throw로 던진 예외를 catch로 잡는 흐름을 보았습니다. 실제 코드에서는 한 try 블록에 여러 catch를 두어 예외의 종류마다 다르게 처리하는데, 이때 어떤 catch가 선택되는지는 예외 객체의 타입과 catch를 적은 순서로 정해집니다.
이 글은 try와 catch의 문법, catch가 예외의 타입과 맞춰지는 규칙, 상속 관계의 예외를 잡는 순서, 예외를 참조로 잡아야 하는 이유, 그리고 잡은 예외를 다시 던지는 방법을 다룹니다.
기본 문법
try 블록 뒤에 catch 블록을 하나 이상 이어 적습니다. 각 catch의 괄호에는 받을 예외의 타입을 함수 매개변수처럼 적고, catch (...)는 어떤 타입의 예외든 받습니다.
try {
// 예외가 생길 수 있는 코드
} catch (const 타입1& e) {
// 타입1의 예외를 처리
} catch (const 타입2& e) {
// 타입2의 예외를 처리
} catch (...) {
// 그 밖의 모든 예외를 처리
}try 블록이 예외 없이 끝나면 catch 블록들은 모두 건너뜁니다. 예외가 생기면 try 블록의 나머지를 건너뛰고, 위에서부터 차례로 catch를 살펴 처음으로 맞는 하나만 실행합니다. 맞는 catch가 실행되고 정상적으로 끝나면, 실행은 마지막 catch 블록 다음으로 이어집니다.
catch가 예외와 맞춰지는 규칙
catch의 타입 비교는 함수 호출의 인자보다 훨씬 엄격합니다. int를 double로 바꾸는 것 같은 수의 변환은 일어나지 않으며, 허용되는 것은 같은 타입, 파생 클래스에서 기반 클래스로의 변환, 포인터의 몇몇 변환 정도입니다.
#include <exception>
#include <iostream>
#include <string>
int main() {
try {
throw 7;
} catch (double) {
std::cout << "double\n";
} catch (int code) {
std::cout << "int " << code << '\n';
}
try {
throw std::string("retry");
} catch (const std::exception&) {
std::cout << "standard exception\n";
} catch (const std::string& message) {
std::cout << "string " << message << '\n';
}
}int 7
string retrythrow 7은 int 예외이므로, 7을 double로 나타낼 수 있어도 catch (double)은 선택되지 않고 catch (int)가 선택됩니다. std::string은 표준 예외 클래스 std::exception을 상속하지 않으므로 catch (const std::exception&)에 맞지 않고, 다음의 std::string 블록이 선택됩니다. throw "retry"처럼 문자열 리터럴을 던지면 예외의 타입은 const char*가 되어 두 블록 어느 것에도 맞지 않습니다.
예외는 스택을 정리하며 호환 handler를 찾는다
try 실행·throw·첫 호환 catch 검색·벗어나는 범위 정리·처리 후 합류를 순서대로 강조하면 try 원위치 복귀 오해를 줄인다. 정상/전파 대안은 전체도에 남겨야 한다.
C++ · CATCH MATCHING
catch는 선언 순서의 첫 호환 핸들러를 선택한다
throw로 정상 경로를 중단하고, 복구할 수 있는 핸들러까지 지역 객체를 정리한다. catch 선택은 타입의 구체성과 함께 선언 순서에 달려 있다.
- 호출과 지역 객체 생성
try안에서 실패할 수 있는 함수를 호출한다. - throw로 현재 정상 경로 중단
throw 다음 문장과 실패한 호출 뒤의 try 문장은 실행하지 않는다.
- 호환 핸들러까지 스택 풀기
빠져나오는 범위의 완성된 자동 객체를 생성 역순으로 파괴한다.
- 선언 순서로 첫 호환 catch 선택
가장 가까운 활성 try에서 위부터 검사한다. 더 구체적인 타입은 기반 타입보다 앞에 적는다.
- 처리 뒤 다음 문장으로 합류
catch가 정상적으로 끝나면 try-catch 다음으로 이어진다. 재던지기는 바깥 핸들러를 찾는다.
본문의 다중 catch 순서
std::invalid_argument
OutOfRangeException
std::exception
...파생 예외를 먼저 처리하고, 기반 예외와 catch-all을 뒤에 둔다.
기반 타입을 먼저 두면
std::exception
std::invalid_argumentstd::invalid_argument도 앞의 기반 타입 catch에 잡힌다. 런타임이 더 구체적인 catch를 우선하지 않는다.
핸들러가 없거나 noexcept 경계를 넘는 종료 경로는 모든 지역 객체의 정리를 보장하지 않는다. 현재 예외를 다시 보낼 때는 throw;로 원래 예외를 보존한다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 8개 노드와 8개 간선이다. try 호출→throw 조건으로 이어진다. 예외가 없으면 try-catch 다음으로 우회하고 예외가 있으면 가장 가까운 활성 try의 선언 순서 검색→호환 catch 조건으로 이어진다. 호환 catch가 있으면 벗어나는 범위 정리→catch 정상 처리→다음 문장으로 합류한다. 없으면 바깥 활성 try로 전파하며 noexcept나 handler가 없는 종료 경로는 합류하지 않는다. catch가 다시 던지는 경로는 아래 본문의 조건으로 구별한다. 같은 예외에 여러 catch가 이어 실행되거나 throw 다음의 원래 try 문장으로 되돌아오는 화살표는 없다. 모바일도 8개 노드와 8개 관계를 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| try의 호출 handler가 있는 try에서 작업을 시작한다. |
|
|
| throw와 전파 던진 타입에 맞는 첫 catch를 찾아 범위를 정리한다. |
|
|
| 처리 후 합류 catch가 정상적으로 끝난 뒤 try-catch 다음으로 간다. |
|
|
예외는 스택을 정리하며 호환 handler를 찾는다
try 실행·throw·첫 호환 catch 검색·벗어나는 범위 정리·처리 후 합류를 순서대로 강조하면 try 원위치 복귀 오해를 줄인다. 정상/전파 대안은 전체도에 남겨야 한다.
C++ · CATCH MATCHING
catch는 선언 순서의 첫 호환 핸들러를 선택한다
throw로 정상 경로를 중단하고, 복구할 수 있는 핸들러까지 지역 객체를 정리한다. catch 선택은 타입의 구체성과 함께 선언 순서에 달려 있다.
- 호출과 지역 객체 생성
try안에서 실패할 수 있는 함수를 호출한다. - throw로 현재 정상 경로 중단
throw 다음 문장과 실패한 호출 뒤의 try 문장은 실행하지 않는다.
- 호환 핸들러까지 스택 풀기
빠져나오는 범위의 완성된 자동 객체를 생성 역순으로 파괴한다.
- 선언 순서로 첫 호환 catch 선택
가장 가까운 활성 try에서 위부터 검사한다. 더 구체적인 타입은 기반 타입보다 앞에 적는다.
- 처리 뒤 다음 문장으로 합류
catch가 정상적으로 끝나면 try-catch 다음으로 이어진다. 재던지기는 바깥 핸들러를 찾는다.
본문의 다중 catch 순서
std::invalid_argument
OutOfRangeException
std::exception
...파생 예외를 먼저 처리하고, 기반 예외와 catch-all을 뒤에 둔다.
기반 타입을 먼저 두면
std::exception
std::invalid_argumentstd::invalid_argument도 앞의 기반 타입 catch에 잡힌다. 런타임이 더 구체적인 catch를 우선하지 않는다.
핸들러가 없거나 noexcept 경계를 넘는 종료 경로는 모든 지역 객체의 정리를 보장하지 않는다. 현재 예외를 다시 보낼 때는 throw;로 원래 예외를 보존한다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 8개 노드와 8개 간선이다. try 호출→throw 조건으로 이어진다. 예외가 없으면 try-catch 다음으로 우회하고 예외가 있으면 가장 가까운 활성 try의 선언 순서 검색→호환 catch 조건으로 이어진다. 호환 catch가 있으면 벗어나는 범위 정리→catch 정상 처리→다음 문장으로 합류한다. 없으면 바깥 활성 try로 전파하며 noexcept나 handler가 없는 종료 경로는 합류하지 않는다. catch가 다시 던지는 경로는 아래 본문의 조건으로 구별한다. 같은 예외에 여러 catch가 이어 실행되거나 throw 다음의 원래 try 문장으로 되돌아오는 화살표는 없다. 모바일도 8개 노드와 8개 관계를 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| try의 호출 handler가 있는 try에서 작업을 시작한다. |
|
|
| throw와 전파 던진 타입에 맞는 첫 catch를 찾아 범위를 정리한다. |
|
|
| 처리 후 합류 catch가 정상적으로 끝난 뒤 try-catch 다음으로 간다. |
|
|
위 그림처럼 예외가 생기면 가장 가까운 try의 catch들을 선언 순서대로 살펴, 처음 맞는 것을 고르고 그사이의 범위를 정리한 뒤 그 블록을 실행합니다. 맞는 것이 없으면 그 try를 벗어나 바깥의 try로 예외를 전파합니다. 한 예외에 여러 catch가 차례로 실행되거나, catch가 끝난 뒤 throw의 다음 문장으로 돌아가는 일은 없습니다.
상속 관계의 예외와 catch 순서
표준 예외 클래스들은 std::exception을 기반으로 하는 상속 계층을 이룹니다. std::logic_error 아래에 std::invalid_argument, std::out_of_range 등이 있고, std::runtime_error 아래에 std::overflow_error 등이 있습니다. catch는 파생 클래스의 예외를 기반 클래스로도 받으므로, catch (const std::exception&) 하나로 표준 예외를 모두 받을 수 있습니다.
#include <iostream>
#include <stdexcept>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3};
try {
std::cout << v.at(10) << '\n';
} catch (const std::out_of_range&) {
std::cout << "index out of range\n";
} catch (const std::exception&) {
std::cout << "other standard exception\n";
}
}index out of rangev.at(10)은 std::out_of_range를 던지고, 첫 catch가 그 타입과 맞아 선택됩니다. catch는 위에서부터 처음 맞는 것을 고르므로, 구체적인 파생 클래스를 먼저 적고 넓은 기반 클래스를 뒤에 적어야 합니다. 순서를 바꾸어 std::exception을 먼저 적으면 그 블록이 모든 표준 예외를 받아 뒤의 std::out_of_range 블록은 실행될 일이 없으며, GCC는 앞의 블록이 뒤의 블록을 가린다고 경고합니다. what()이 돌려주는 메시지의 내용은 구현마다 다르므로, 이 예제는 메시지 대신 직접 정한 문장을 출력했습니다.
예외는 const 참조로 잡습니다
catch (std::exception e)처럼 값으로 받으면, 던진 객체가 catch의 매개변수로 복사됩니다. 이때 다형성과 가상 함수에서 본 객체 슬라이싱이 일어나, 파생 클래스의 예외가 기반 클래스 부분만 남은 객체가 됩니다. 그러면 파생 클래스가 재정의한 what()도 쓰이지 않습니다.
그래서 예외는 catch (const std::exception& e)처럼 const 참조로 받습니다. 참조로 받으면 복사가 없고, 원래의 예외 객체를 가리키므로 가상 함수가 실제 타입에 맞게 호출됩니다. 예외 객체를 바꿀 일은 거의 없으므로 const도 붙입니다.
예외를 다시 던지기
catch 블록 안에서 할 수 있는 일을 한 뒤, 예외를 바깥으로 계속 전달해야 할 때가 있습니다. 예를 들어 실패를 기록만 하고, 실제 처리는 호출한 쪽에 맡기는 경우입니다. 이때는 인자 없이 throw;라고 적어, 지금 처리 중인 예외를 그대로 다시 던집니다.
#include <iostream>
#include <stdexcept>
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
void inner() {
throw AppError("disk full");
}
void middle() {
try {
inner();
} catch (const std::exception& e) {
std::cout << "middle saw: " << e.what() << '\n';
throw;
}
}
int main() {
try {
middle();
} catch (const AppError& e) {
std::cout << "main caught AppError: " << e.what() << '\n';
} catch (const std::exception&) {
std::cout << "main caught exception\n";
}
}middle saw: disk full
main caught AppError: disk fullmiddle은 예외를 std::exception으로 받아 메시지를 출력한 뒤 throw;로 다시 던지고, main은 그 예외를 원래 타입인 AppError로 잡습니다. throw;는 받은 예외 객체를 복사하지 않고 그대로 다시 던지기 때문입니다. using std::runtime_error::runtime_error;는 상속의 개념과 구현에서 본 생성자 상속으로, 기반 클래스의 생성자를 그대로 쓰게 합니다.
더 깊이: 생성자의 초기화 목록에서 난 예외를 잡으려면
멤버 초기화 목록에서 던진 예외는 생성자 본문 안의 try로는 잡을 수 없습니다. 멤버는 본문이 시작되기 전에 초기화되기 때문입니다. 이럴 때는 Widget() try : name(load()) { } catch (const std::exception& e) { ... }처럼 함수 본문 전체를 감싸는 함수 try 블록(function try block)을 쓰며, 이 catch는 초기화 목록과 본문에서 난 예외를 모두 받습니다.
생성자와 소멸자의 함수 try 블록에는 특별한 규칙이 있습니다. catch 블록의 끝에 이르면 처리 중이던 예외가 저절로 다시 던져지므로, 이 catch는 예외를 삼켜 객체가 만들어진 것처럼 할 수 없고 기록을 남기거나 다른 예외로 바꾸어 던지는 정도만 할 수 있습니다. 또 이 catch 안에서 그 객체의 비정적 멤버나 기반 클래스를 쓰는 것은 정의되지 않은 동작입니다. 이 규칙들은 cppreference의 try 블록 설명에 정리되어 있습니다.
정리
- 한
try블록에 여러catch를 둘 수 있으며, 예외가 생기면 위에서부터 처음 맞는 하나만 실행됩니다. catch는 수의 변환을 하지 않고, 같은 타입이나 파생 클래스에서 기반 클래스로의 변환 정도만 허용합니다.- 맞는
catch가 없으면 바깥의try로 예외를 전파하며,catch가 끝나면 마지막catch다음으로 이어집니다. - 표준 예외는
std::exception을 기반으로 하는 계층이며, 구체적인 파생 클래스의catch를 먼저 적습니다. - 예외는 슬라이싱을 피하도록
const참조로 잡습니다. - 잡은 예외를 그대로 전달하려면 인자 없는
throw;를 씁니다.
연습 문제
1. 던지는 타입 바꾸기
catch_matching.cpp에서 throw 7;을 throw 7.0;으로 바꾸면 무엇이 출력되나요? throw std::string("retry");를 throw "retry";로 바꾸면 어떻게 되나요?
해설 보기
throw 7.0은 double 예외이므로 첫 블록이 선택되어 double이 출력됩니다. throw "retry"는 const char* 예외여서 std::exception과도 std::string과도 맞지 않으므로, 그 try에서 처리되지 않고 main 밖으로 나가 std::terminate로 프로그램이 끝납니다. 이런 예외도 받으려면 catch (const char*)나 catch (...)를 추가해야 합니다.
2. 거꾸로 적은 catch
catch_order.cpp에서 두 catch의 순서를 바꾸면 무엇이 출력되나요?
해설 보기
std::out_of_range는 std::exception의 파생 클래스이므로, 먼저 적힌 catch (const std::exception&)가 맞아 other standard exception이 출력됩니다. 뒤의 std::out_of_range 블록은 어떤 예외에도 선택될 수 없게 되며, GCC는 이를 경고합니다.
3. throw e;로 다시 던지기
rethrow.cpp의 middle에서 throw;를 throw e;로 바꾸면 main은 무엇을 출력하나요?
해설 보기
throw e;는 e의 정적 타입인 std::exception으로 새 예외 객체를 복사해 던지므로, AppError 부분이 잘린 std::exception 객체가 던져집니다. 그래서 main의 catch (const AppError&)는 맞지 않고, 두 번째 블록이 선택되어 main caught exception이 출력됩니다. 받은 예외를 그대로 전달하려면 throw;를 씁니다.
다음 글 예외 던지기에서는 어떤 객체를 예외로 던질지, 표준 예외 클래스를 상속해 예외 클래스를 만드는 방법, 그리고 noexcept와 예외 안전성을 다룹니다.