C++ 기초부터 마스터까지 — 15화
예외가 발생해도 자원을 안전하게 관리하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
파일을 읽고 메모리를 할당한 뒤 중간 코드에서 문제가 생기면 이미 얻은 자원을 정리해야 한다. 모든 return과 예외 경로마다 정리 코드를 직접 작성하면 빠뜨리기 쉽다.
예외가 발생하면
예외는 현재 위치에서 처리할 수 없는 실패를 호출한 쪽으로 전달하는 방법이다.
Item LoadItem(int id)
{
if (id <= 0)
{
throw std::invalid_argument("id must be positive");
}
return ReadItemFromFile(id);
}
try
{
Item item = LoadItem(inputId);
}
catch (const std::exception& error)
{
std::cerr << error.what() << '\n';
}
throw가 실행되면 현재 줄 아래의 코드는 건너뛰고 일치하는 catch를 찾는다. 그 과정에서 이미 생성된 지역 객체의 소멸자가 호출된다.
모든 실패에 예외가 필요한 것은 아니다. 아이템 검색 실패처럼 예상 가능한 결과는 optional이나 bool이 더 분명할 수 있다.
RAII로 정리하기
자원의 수명을 객체의 수명에 묶으면 정상 반환과 예외 경로를 따로 정리하지 않아도 된다.
std::string ReadConfig(const std::filesystem::path& path)
{
std::ifstream file(path);
if (!file)
{
throw std::runtime_error("config open failed");
}
std::string text(
std::istreambuf_iterator<char>{file},
std::istreambuf_iterator<char>{}
);
return text;
}
중간에 문자열 생성이 실패해 예외가 발생하더라도 ifstream의 소멸자가 파일을 닫는다. 동적 객체는 unique_ptr, 잠금은 std::lock_guard, 컨테이너 메모리는 vector가 같은 역할을 한다.
void UpdateInventory()
{
std::lock_guard<std::mutex> lock(inventoryMutex);
RebuildInventory();
}
RebuildInventory에서 예외가 발생해도 lock의 소멸자가 mutex를 해제한다. 직접 lock과 unlock을 짝지을 때 생길 수 있는 실수를 줄여준다.
예외 안전성
예외가 발생했을 때 함수가 어느 상태를 보장하는지 생각해야 한다.
- 기본 보장: 자원 누수는 없고 객체는 사용할 수 있는 상태를 유지한다.
- 강한 보장: 작업이 실패하면 호출 전 상태를 그대로 유지한다.
- 예외 없음 보장: 함수가 예외를 밖으로 보내지 않는다.
새 상태를 임시 객체에 완성한 뒤 마지막에 교체하면 강한 보장을 만들기 쉽다.
void Inventory::ReplaceItems(std::vector<Item> newItems)
{
items.swap(newItems);
}
호출 인자를 만드는 도중 실패하면 기존 items는 바뀌지 않는다. 함수 안에서는 swap으로 완성된 새 목록과 기존 목록을 교체한다.
noexcept
예외를 밖으로 던지지 않는 함수에는 noexcept를 표시할 수 있다.
Player(Player&& other) noexcept = default;
Player& operator=(Player&& other) noexcept = default;
vector는 원소를 재할당할 때 안전하게 이동할 수 있는지 판단한다. 이동 생성자가 noexcept라면 복사 대신 이동을 선택할 가능성이 높다.
하지만 내부에서 예외가 날 수 있는데 noexcept를 붙이면 예외가 밖으로 나가는 순간 std::terminate가 호출된다. 성능을 위해 무작정 붙이는 키워드가 아니라 실제로 예외를 전달하지 않는다는 약속이다.
예외 안전성의 핵심은 catch를 많이 작성하는 것이 아니다. 자원은 RAII 객체가 맡고, 실패하더라도 기존 상태를 망가뜨리지 않는 순서로 작업을 구성하는 것이다.