01. Exception
C는 Exception이 없어서 모든 에러를 return과 &argument로 해결한다
C++는 std::optional, std::variant, std::tuple/pair를 통해 해결한다
그리고 에러를 handling 하는 Exception 개념이 도입되었다
<exception>을 인클루드 하고 try, catch, throw를 통해 사용한다
error의 종류에 따라 다양한 exception의 자식 클래스들을 활용할 수 있다
#include <iostream>
#include <exception>
int divide(int a, int b)
{
if (b == 0)
{
//throw std::exception();
throw std::runtime_error("divide by 0");
}
return a / b;
}
int main()
{
try
{
std::cout << divide(10, 0) << std::endl;
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
std::cout << "exception catched" << std::endl;
}
}
# object를 throw
object를 throw 하기 위해서는 아래의 코드처럼 throw를 하고
catch의 인자에 해당하는 오브젝트의 참조형을 작성하면 된다
하지만 이처럼 object에 try, catch, throw를 사용해서는 안된다
exception은 오직 에러를 handling 하기 위해 사용하는 것이고
또 다른 object를 던지고 받는 용도로 사용하는 것이 아니다
#include <iostream>
#include <exception>
class Cat
{
public:
Cat()
{
std::cout << "cat c" << std::endl;
}
~Cat()
{
std::cout << "cat d" << std::endl;
}
};
int divide(int a, int b)
{
if (b == 0)
{
//throw std::exception();
//throw std::runtime_error("divide by 0");
throw Cat();
}
return a / b;
}
int main()
{
try
{
std::cout << divide(10, 0) << std::endl;
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
std::cout << "exception catched" << std::endl;
}
catch (const Cat& c)
{
std::cout << "cat catched" << std::endl;
}
}
# catch (...)
추가적으로,
catch의 인자에... 을 사용하면
모든 exception의 형태를 인자로 받아준다
catch (...)
{
std::cout << "all exception catched" << std::endl;
}
02. Stack Unwinding
# exception의 내부 동작
여러 스택들이 상황에 맞게 쌓여있다는 가정하에
try 구문이 실행되고 throw 여부가 결정이 된다
throw가 실행되면, Exception Object가 만들어지고
try & catch 구문을 발견할 때까지
stack unwinding 하면서 스택들이 pop 된다
그리고 try & catch 구문이 있는 스택에서
catch구문의 인자가 Exception Object를 가리키게 된다
try & catch 구문이 존재하지 않는다면
std::terminate 호출되면서 프로세스 종료된다
# exception의 장점
exception을 사용하지 않고
error code를 return 하고 check 하는 방식으로 사용하면,
function call을 할 때마다 불러진 함수의 retrun 값을 check를 하면서
하위 스택으로 이동해야 해서 코드가 복잡해진다
반면, exception을 사용하면 stack unwinding으로
코드의 복잡함 없이 직관적인 예외처리가 가능하다
# exception의 performance
exception이 던져지지 않는 path에서는
성능에 전혀 문제가 없다
exception이 throw 되는 경우에는
일반적인 함수 종료의 path와는 다른 path이기에
overhead가 발생할 수 있다
반복문에서는 사용하지 않는 것이 좋고
성능을 중시해야 하는 경우에는 프로파일을 통해 확인하는 것이 좋다
# exception의 문제점
대표적인 예시로는 Memory Leak이 있다
스마트 포인터를 이용하면 해결 가능하다
#include <iostream>
#include <exception>
#include <memory>
class Cat
{
private:
int mAge = 10;
};
int divide(int a, int b)
{
if (b == 0)
{
throw std::runtime_error("divide by 0");
}
return a / b;
}
void f()
{
//std::unique_ptr<Cat> cp = std::make_unique<Cat>();
Cat* cp = new Cat();
std::cout << divide(10, 1) << std::endl;
delete cp;
}
int main()
{
try
{
f();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
}
이처럼 exception에서 신경 써줘야 하는 부분은
일반적인 함수 종료의 path와는 다른 path이기에
그 점을 인지하고 사용하지 않는다면, overhead가 발생할 수 있다
하지만 stack unwinding은 그 과정 중에
내부 object의 destructor는 반드시 호출한다
따라서, 스마트 포인터와 같은 RAII를 지키면서
코드를 작성하면 안전한 코드가 될 수 있다
03. Safety Guarantees
exception을 안전하게 사용하기 위해선
아래 3가지 조건을 지켜야 한다
1) Basic Exception Safety
2) String Exception Safety
3) No-throw guarantee
// 4) No Exception Safety: 위 3가지를 다 안 지키는 경우
1) Basic Exception Safety
Resource Leak이 없는데
exception이 던져졌을 때, state가 변한다면
basic exception safety를 준수하는 것을 의미한다
#include <iostream>
#include <exception>
#include <memory>
int g = 0;
int divide(int a, int b)
{
if (b == 0)
{
throw std::runtime_error("divide by 0");
}
return a / b;
}
void f()
{
g = 1;
divide(10, 0);
}
int main()
{
std::cout << "g: " << g << std::endl;
try
{
f();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
std::cout << "g: " << g << std::endl;
}
2) String Exception Safety
Resource Leak이 없는데
exception이 던져졌을 때, state가 변하지 않으면
strong exception safety 준수하는 것을 의미한다
strong exception safety를 지키고 싶다면
exception이 던져지는 함수는 가장 앞에 있어야 한다
그 결과를 저장하고 싶다면, 지역변수로 저장해야 한다
#include <iostream>
#include <exception>
#include <memory>
int g = 0;
int divide(int a, int b)
{
if (b == 0)
{
throw std::runtime_error("divide by 0");
}
return a / b;
}
void f()
{
int a = divide(10, 0);// exception이 발생하는 함수가 젤 위에
int b = divide(10, 1);
int c = divide(10, 2);
int d = divide(10, 3);
g = 1;// exception이 발생할 수 있는 함수 아래에
}
int main()
{
std::cout << "g: " << g << std::endl;
try
{
f();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
std::cout << "g: " << g << std::endl;
}
3) No-throw guarantee
exception이 던져지지 않는 경우를 의미한다
아래의 경우 f 함수는 main에서 throw 되지 않으며
f 함수는 No-throw guarantee를 준수하고 있다고 말할 수 있다
#include <iostream>
#include <exception>
#include <memory>
int g = 0;
int divide(int a, int b)
{
if (b == 0)
{
throw std::runtime_error("divide by 0");
}
return a / b;
}
void f()
{
try
{
int a = divide(10, 0);
}
catch (const std::exception& e)
{
// 여기서 잡힌다
}
}
int main()
{
std::cout << "g: " << g << std::endl;
try
{
f();
}
catch (const std::exception& e)// 여기서 잡히지 않는다
{
std::cout << e.what() << std::endl;
}
std::cout << "g: " << g << std::endl;
}
# throw로 던지면 안 되는 함수
default constructor, destructor, swap opreation, move opreation 함수는
절대 exception을 던지면 안 된다
위의 경우는 exception이 두 개 이상 던져지게 되고
이를 C++는 handling 할 수 없다
04. Usages
# exception safety guarantees
stack unwinding의 path는 일반적인 함수 종료의 path와는
다르기 때문에
exception safety guarantees를 준수하지 않으면
resource leak이 발생할 수 있다
# exception을 쓰면 안 되는 경우
- regular error report
: 자주 exception이 뱉어지는 경우
- internal error handling
: 함수 내부에서 에러가 발생하면, 최대한 내부에서 처리하면 된다
- nullptr, out of range
: 이런 경우가 발생하는 것은 버그, 실수에 가깝고 이는 에러로 뱉는 것이 아닌 코드 수정을 해야 한다
- never happen
: 절대 발생하지 않을 경우를 굳이 작성할 필요는 없다)
# exception을 쓰면 되는 경우
C++ constructor는 return type이 존재하지 않는다
따라서 object가 생성이 될 때 에러가 발생하면
exception을 사용한 에러 리포트만 가능하다
예시로는
서버에서 packet을 받았는데 불안정한 경우나
파일 시스템에서 파일이 존재하지 않는 경우 같은 상황에서 활용할 수 있다
# google c++ style guide
구글 c++ style guide에서는 exception을 사용하지 않는다
가장 큰 이유는
파이썬은 exception을 권장하는 언어이고
garbage collector가 있어서 최소한의 Basic Exception Safety를 준수한다
C++에서는 RAII로 코드를 작성하지 않으면
Basic Exception Safety가 지켜지지 않을 가능성이 높다
이처럼 Exception Safety가 없는 상황에서
Exception을 사용하면 문제가 발생할 확률이 높아진다
'C++ > [노코프] C++' 카테고리의 다른 글
[C++ NOTE] Types (0) | 2024.04.01 |
---|---|
[C++ NOTE] Functional Programming (0) | 2024.03.31 |
[C++ NOTE] Template (0) | 2024.03.30 |
[C++ NOTE] Smart Pointer (0) | 2024.03.29 |
[C++ NOTE] Inheritance (0) | 2024.03.28 |