C & C++/C++

[C++] 예외 처리

거북이 코딩 2024. 8. 8. 16:50

Ch 05. 예외 처리

전통적인 예외 처리

예외가 발생했을 때 사용할 수 있는 방법은 여러 가지가 있다.

// 프로그램을 종료하는 방법

std::abort(); // 비 정상 종료
exit(123); // 코드와 함께 종료
return 123; // 코드와 함께 종료

// 반환하여 오류를 알려주는 방법
printf(); // 에러가 나면 -1 반환, 정상이면 글자 수 반환
std::cout<<10;
cout.fail(); // 에러가 나면 true 반환

이외에도 전역변수로 errorCode 확인하기 등 방법이 있지만 예외처리가 강제되지 않기 때문에 예외가 무시될 수 있다. 예외처리를 강제라기 위한 메커니즘으로 C++에서 try catch를 사용한다.

#include <iostream>

using std::cout;
using std::endl;

int divide(double d, double v, double& result) // 결과를 다른 변수에 저장하기
{
    if (v == 0)
    {
        result = 0;
        return -1;
    }
    result = d / v;
    return 0;
}

int errorCode = 0;
double divide(double d, double v) // 전역 변수에 에러 여부 저장하기
{
    if (v == 0)
    {
        errorCode = -1;
        return 0;
    }
    errorCode = 0;
    return d / v;
}

int main()
{
    // 아래의 예외 처리 방법은 예외 처리가 강제 되지 않는다.
    // printf 가 실패했지만 실패한 것에 대한 예외 상황을 처리하지 않고 작성하더라도 눈치채지 못할 수 있다.
    // 따라서 실패 시 실패에 대한 처리를 강제하게 하는 메커니즘이 필요
    double result;
    int errorCode = divide(10, 0, result); // 에러 발생 시 에러 코드
    if (errorCode > 0)
        cout << result << endl;
    
    double result = divide(10, 0);
        if (errorCode > 0) // 전역적으로 사용하는 변수, 멀티 쓰레드에서 문제 발생
            // 에러 처리
}

try catch

예외가 발생했을때 throw를 사용한다.

throw "Divide by zero";
// throw가 발생하면 내부적으로 std::terminate(); 가 호출된다.
std::terminate(); // teminate는 abort와 다르게 수정할 수 있다.

void terminateFunc()
{
    cout << "terminate" << endl;
}
std::set_terminate(terminateFunc);

try 블록내에서 throw 된다면 catch 블록에서 잡아서 예외를 처리해 줄 수 있다.

#include <iostream>

using std::cout;
using std::endl;

double divide(double d, double v)
{
    if (v == 0)
    {
        throw "Dirived by zero"; // 처리 되지 않으면 std::terminate
    }
    return d / v;
}

int main()
{
    try
    {
        cout << divide(10, 0) << endl;
    }
    
    catch (const char* e) // 처리할 예외의 타입
    {
        cout << e << endl;
    }
    
    catch (...) // 처리할 예외타입이 무엇인지 알지 못할 떄
    {
        cout << "..." << endl;
    }
}

상속 관계에 있는 객체를 던질 경우

class Parent {};
class Child : public Parent {};

int main()
{
    try
    {
        throw Child();
    }
    
    catch (Parent&) // 부모 타입으로 받을 수 있다
    {
        cout << "Parent" << endl;
    }
    
    catch (Child&) 
    // catch 순서대로 체크를하기 때문에 위쪽 catch에 호환이 되기 때문에 위쪽 catch로 가게 된다.
    {
        cout << "Child" << endl;
    }
}

stack unwinding 스택 풀기 : throw가 되면 catch 블록을 만날 때까지 stack을 제거한다.

class Test
{
public:
    ~Test() { cout << "test" << endl; }
};

void func1()
{
    Test t; // throw되면 자동 지역 변수는 모두 파괴 된다.
    divide(10, 0);
    // throw로 인해 이 줄은 실행되지 않는다.
}

void func0()
{
    Test t;
    func1();
    // throw로 인해 이 줄은 실행되지 않는다.
}

int main()
{
    try
    {
        func0(); 
        // divide에서 예외가 발생했지만 예외를 처리할 수 있는 곳까지 함수 스택을 되돌린다
    }
    catch (const char* e)
    {
        cout << e << endl;
        throw; // 받은 예외를 rethrow 할 수 있다. 그대로 다시 던진다.
    }
}

함수 자체에 try를 거는 경우

// 함수 자체에 try를 걸 수도 있다
void foo() try
{

}
catch (const char* e)
{

}

std::exception 을 상속해서 예외를 만들면 좋다

// std::exception을 상속해서 예외 객체를 만들자
// 표준 라이브러리의 예외들도 std::exception을 상속했음
std::runtime_error; // 표준 라이브러리의 예외 중 하나

class CustomException : public std::exception
{
public:
		virtual const char* what() const override
		{
				return "Custom";
		}
}

//std::exception 의 what()을 오버라이드 해서 무슨 예외인지 표기한다.

RAII

Stack Unwinding, 예외가 발생했을 때 스택을 파괴하는 과정에서 동적 할당된 객체가 있을 경우 메모리가 정상적으로 해제되지 않는다. 이러한 문제를 해결하기 위해 RAII를 적용한다.

#include <iostream>

using std::cout;
using std::endl;

class Test
{
public:
    ~Test() { cout << "~Test" << endl; }
};

void func()
{
    throw "exception";
}

int main()
{
    try
    {
        int* i = new int; // 메모리 누수 발생
        func();
    }
    catch (const char* e)
    {
        cout << e << endl;
    }
}

RAII를 사용하지 않을 경우 해결 방법

int main()
{
    int* i = nullptr
    try
    {
        i = new int;
        func();
    }
    catch (const char* e)
    {
		    delete i; // catch가 여러개일 경우 일일히 해주어야하고 객체가 여러개일 경우
							    // 매우 불편해진다.
        cout << e << endl;
    }
}

RAII : Resource Acquisition Is Initialization 기법을 사용하는 방법

#include <memory>

class RAII
{
public:
    int* i;
    RAII() // 생성 시 동적할당
    {
        i = new int;
    }
    ~RAII() // 파괴시 자동으로 메모리를 해제
    {
        cout << "~RAII" << endl;
        delete i;
    }
};

class Test
{
public:
    ~Test() { cout << "~Test" << endl; }
};

void func()
{
    throw "exception";
}

int main()
{
    try
    {
        RAII raii;
        func();
    } // 블럭을 벗어나면서 RAII의 소멸자가 호출되면서 내부에 할당한 동적 객체가 해제된다
    catch (const char* e)
    {
        cout << e << endl;
    }

    try
    {
        // unique_ptr을 이용해 동적할당에 대한 RAII를 사용할 수 있다
        std::unique_ptr<Test> test(new Test()); // memory를 include
        func();
    }
    catch (const char* e)
    {
        cout << e << endl;
    }
}

unique_ptr을 사용하면 지역을 벗어나며 자동으로 메모리가 해제된다.

noexcept

예외가 발생하지 않는다는 의미이다. 컴파일러에게 이 사실을 알려주어 좀 더 최적화를 할 수 있다.

int main noexcept
{
		return 0;
}
void func0() noexcept(true) {
    // 이 함수도 예외를 던지지 않습니다.
}

void func1() noexcept(false) {
    // 이 함수는 예외를 던질 수 있습니다.
}

noexcept(bool) bool에 다른 조건식을 넣어 조건부 noexcept를 선언할 수 있다.
#include <iostream>

using std::cout;
using std::endl;

// 소멸자는 기본적으로 noexcept 함수

void func() noexcept // 해당 함수에서는 예외가 나지 않는다고 선언, 컴파일러가 최적화 할 수 있다
{
    //throw 1; 
    // noexcept 선언한 함수에서 throw 된다고 하더라도 try catch로 처리할 수 없다
}

int main()
{
    try
    {
        func();
    }
    catch (int e)
    {

    }

    // 몇몇 STL 함수에서는 noexcept로 지정된 함수가 제공되지 않는 경우 성능 손실이 생길 수 있다
}

noexcept 의 함수 포인터

using FuncType = void (*)() noexcept;

void safeFunction() noexcept {
    // 안전한 함수
}

FuncType f = safeFunction;

noexcept 선언된 함수 포인터에는 noexcept 함수만 올 수 있다.
선언 되지 않은 함수에는 noexcept 유무에 관계 없이 함수가 올 수 있다.