본문 바로가기
연구소👨‍💻/CS연구소

[C++] 예외 처리

by 신그자체김상범 2024. 6. 26.

예외처리

예외처리는 프로그래밍 언어에서 필수다. 단순 알고리즘을 풀면서도 수많은 에러와 마주하고 실제 서비스를 위한 프로그램에서는 예상하지도 못한 에러가 발생할 때도 많을 것이다.

최근 SRS라는 오픈소스 프로젝트를 읽고있는데 그쪽의 코드를 보면 실제 로직보다 에러처리가 훨씬 많다는걸 알 수 있었다. 그러다보니 내가 나아가야 할 방향도 이런 에러들을 관리하는 방향이라는 생각을 하게 됐고 정리나 한 번 해보려고한다.

try, catch, throw, noexcept

try - catch는 다른 언어에서도 많이 사용되는 쌍이다. 일단 무슨에러가 발생할지 모를 때 로직을 감싸고 에러가 발생했을 경우에 할 로직을 따로 처리해놓는 것이다

    try {
        int result = divide(num1, num2);
        std::cout << "The result is: " << result << std::endl;
    } catch (const char* msg) {
        std::cerr << "Error: " << msg << std::endl;
    }

그리고 함수에서 에러는 try - catch로 감싸져있지 않을 경우 바로 std::terminate를 불러서 프로세스를 종료시킨다.

int divide(int a, int b) {
    if (b == 0) {
        throw "Division by zero!";
    }
    return a / b;
}

try {
	...
}
catch (const char* e) {
  cout << "Error: " << e << endl;
}

그리고 throws에도 타입이 존재하며 catch는 이 throw의 변수에 맞는 값을 리턴해야한다.

그리고 throw가 일어나면 해당 함수와 함수에서 선언된 변수들은 전부 스택 세그먼트에서 사라진다. static으로 선언된 변수는 예외이다.

그리고 throw를 받은 스코프는 catch구문이 존재할 경우 그 영역으로 이동하고 그렇지 않을경우는 프로그램을 강제로 종료한다

noexcept는 제작한 함수에서 에러가 발생하지 않을 것이라 명시하는 코드다.

객체의 소멸자나 이동 생성자등에 사용될 수 있으며 함수 뒤에도 사용될 수 있다. noexcept를 사용하면 에러 처리에 대한 오버헤드가 줄어서 성능이 더 좋아진다고 한다 다만 그럼에도 예상하지 못한 에러가 발생했을 경우엔 바로 프로세스를 종료한다.

그리고 아까 소멸자, 이동 연산자등에서 noexcept를 설정해 놓은것이 확인된다면 본인도 noexcept 선언을하는 조건부 noexcept 도 존재한다.

#include <iostream>
#include <utility>
#include <type_traits>

class NoThrowMove {
public:
    NoThrowMove() {}
    NoThrowMove(NoThrowMove&&) noexcept {}  // 이동 생성자가 noexcept
};

class ThrowMove {
public:
    ThrowMove() {}
    ThrowMove(ThrowMove&&) {}  // 이동 생성자가 noexcept가 아님
};

template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::declval<T>()))) {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

int main() {
    NoThrowMove a, b;
    ThrowMove c, d;

    std::cout << std::boolalpha;
    std::cout << "NoThrowMove swap is noexcept: " << noexcept(swap(a, b)) << std::endl;  // true
    std::cout << "ThrowMove swap is noexcept: " << noexcept(swap(c, d)) << std::endl;    // false

    return 0;
}

위에서 NoThrow 클래스는 noexcept가 선언되어 있고 Throw 클래스는 선언되어있지 않다.

그런 상황에서 어떤 타입 객체의 교환에 대한 함수 swap이 noexcept인지 선언하는데에는

T(std::declval<T>()) 가 참이어야한다는 조건이 들어간다

noexcept(조건) 은 true 혹은 false를 반환하는 함수다

그리고 noexcept(true), noexcept(false)는 이 함수를 noexcept로 선언할지 설정한다.

표준 예외처리 라이브러리

C++은 standard Exceptions라는 예외에 대한 라이브러리를 제공한다.

대표적인 개념으로

  • std::exception: 모든 에러의 기본형.
  • std::logic_error: 로직에러
  • std::runtime_error: 프로그램 실행중에 발생할 수 있는에러
  • std::out_of_range : 범위를 넘어서는 접근

등의 예외를 제공한다. 그리고 유저에 따라서

Exit Codes 개념

하지만 내가 보는 오픈소스는 이런 예외처리를 사용하지 않고 있었다. 오히려 숫자와 함수 리턴값으로 예외상황이 발생했는지 판단하는 코드가 많았다.

srs_error_t run_hybrid_server(void * /*arg*/)
{
    srs_error_t err = srs_success;

    // Create servers and register them.
    _srs_hybrid->register_server(new SrsServerAdapter());

#ifdef SRS_SRT
    _srs_hybrid->register_server(new SrsSrtServerAdapter());
#endif

#ifdef SRS_RTC
    _srs_hybrid->register_server(new RtcServerAdapter());
#endif

    // Do some system initialize.
    if ((err = _srs_hybrid->initialize()) != srs_success)
    {
        return srs_error_wrap(err, "hybrid initialize");
    }
    
    ...
    
}
  

이 방식은 에러를 숫자로 바꿔서 return과 함께 사용하는 exits cod라는 개념이다. 함수가 잘 실행되었으면 0 아니면 다른 정수를 return으로 반환해 여러 함수의 연속에서 문제를 파악한다.

Exits Code 방식은 예외처리보다 가볍고 호환성이 좋다는 특징이 있다.

에러처리가 제공되지 않는 c언어와의 호환이나.

에러처리에 사용되는 리소스를 과하다고 임베디드와 같은 여기는 프로그램에서는

리턴하는 코드를 사용해서 에러를 처리한다.