레이블이 exception인 게시물을 표시합니다. 모든 게시물 표시
레이블이 exception인 게시물을 표시합니다. 모든 게시물 표시

2017-02-10

[c++] 생성자에서 예외가 발생하면 어떻게 될까

 R.A.I.I.를 사용하다 보면, 생성자에서 복잡한 일을 해야 할 경우가 종종 생긴다. 복잡한 일은 실패할 수도 있고, 그 경우 예외가 발생하기도 한다. 여기서 궁금한 점이 생긴다. 생성자에서 예외가 던져져도 아무 문제 없을까?

 우선 걱정되는 것은 메모리 릭이다. 하지만 다행히도 메모리 릭은 발생하지 않는다. 스택에 생성된 변수는 스택 unwind를 통해 메모리가 해제되며, new operator를 통해 힙에 할당하다 예외가 발생한 경우에도 new operator가 알아서 메모리를 해제하고 nullptr를 리턴해준다.

 그다음으로 걱정되는 것은 멤버 변수의 소멸자가 잘못 불리지 않을까 하는 것이다. 하지만 이 역시 문제없다. 예외가 발생한 시점에서 멤버 변수는 초기화가 완료된 멤버 변수, 초기화 중이었던 멤버 변수, 초기화되지 않은 멤버 변수 3가지로 나눌 수 있다.
 초기화가 완료된 멤버 변수는 말 그대로 생성자가 불렸고 정상적으로 메모리 할당을 완료한 멤버 변수다. 위 코드에서는 b에 해당하는데, 이들은 예외가 발생하면 이 변수들은 정상적으로 소멸자가 불리며 리소스를 해제한다.
 초기화 중이었던 변수는 위 코드에서 c에 해당하는 멤버 변수다. 위 코드는 E 클래스의 생성자에서 C인 변수 c의 생성자가 예외를 발생시켰다. 이 경우 E 클래스 입장에서 c는 초기화 중인 변수가 된다. 위 코드와 같이 멤버 변수의 초기화 중에서 예외가 발생하면 초기화 중인 변수가 1개 존재하지만, 생성자의 본체에서 예외가 발생하면 초기화 중인 변수는 존재하지 않는다.
 마지막으로 초기화되지 않은 변수의 경우 생성자가 불리지 않았으니 소멸자가 불리지 않는다. 하지만 이 경우는 아직 생성자가 불리지 않았기 때문에 소멸자가 안 불리는 것이 맞다.

 상속받는 경우는 어떨까? 부모 클래스에서 할당한 리소스는 정상적으로 해제될까? 당연히 이 경우도 아무 문제없다. 생성자에서 예외가 발생했을 때, 초기화가 완료 된 멤버 변수의 소멸자가 전부 불리고 나면, 부모 클래스의 소멸자가 불리며 리소스를 해제한다. 따라서 위의 코드를 실행시키면 다음과 같은 결과가 나온다.

 다시 처음 질문으로 돌아가 보자. 생성자에서 예외를 발생시키는 것은 아무 문제 없을까?
 위에서 보았듯이 아무런 문제도 없다...... 라고 말하고 싶지만, 사실 여기는 한 가지 가정이 필요하다. 클래스가 잘 설계되었다는 가정 하에서 생성자에서 예외가 던져져도 아무런 문제도 없다.

 그렇다면 잘 설계되지 않은 클래스는 무엇이고, 이 경우 어떤 문제가 있을?
 위의 코드를 보자. B의 생성자에서 A를 힙에 할당하고, 소멸자에서 명시적으로 delete를 호출하여 해제한다. 생성자에서 예외가 발생하지 않는다면 이 코드는 문제없다. B가 할당될 때 예외가 발생하면, A가 할당되며, B가 소멸할 때, A가 소멸한다.
 하지만 B의 생성자에서 예외가 발생하면 문제가 된다. B의 멤버 변수들을 정리할 때 a는 단순히 A의 포인터이기 때문의 A의 소멸자가 불리지 않는다. 또한, B가 초기화되지 않았기 때문에 B의 소멸자가 불릴 일도 없고 a는 메모리 릭이 된다. 이것을 막기 위해서는 아래와 같이 A의 라이프 타임을 B와 일치시켜야 한다.

2015-07-28

[Monad] 사용 예제 - Try : 예외 처리하기

 현대의 대부분의 언어는 예외 처리를 위해서 try-catch 시스템을 사용한다. 예외가 발생할 수 있는 코드를 try 블록에 집어넣고, 예외를 throw하면 catch 블록에서 예외를 잡아서 처리하는 방식으로, 사실상 현대의 언어들이 예외를 처리하는 방식의 de facto라고 할 수 있다. 하지만 try-catch 시스템에는 여러 가지 문제가 있다.

 우선 다른 함수를 호출할 때, 어떤 예외가 발생할지 모른다. 그래서 Java 같은 언어는 함수의 시그니쳐에 발생 가능한 예외를 적는 checked exception이라는 개념을 만들었지만, RuntimeException은 어떤 예외가 발생할지 모른다거나, 모든 예외를 하나하나 등록하기 귀찮아서 그냥 Exception이 발생한다고 적거나 하는 이유로 그다지 쓸모없다는 인식이 강하고 C#을 비롯한 다른 언어들에서는 사용되지 않는다. 그저 API 문서에 함수가 어떤 예외를 발생시킬 수 있는지 적을 뿐이다.

 게다가 try-catch 시스템은 예외를 던지는 것이기 때문에 컨트롤 플로우가 뛰게 된다. 물론 현대 언어에서는 클로져나 람다 함수가 자주 사용되기 때문에 컨트롤 플로우가 직선적으로 흐르지 않는다. 하지만 try-catch 시스템은 도가 지나치다. 예외를 던지면, 예외를 잡을 때까지 컨트롤 플로우가 거슬러 올라간다. 그래서 try-catch에 의한 예외 시스템을 가독성이라는 측면에서 gotosetjmp/longjmp와 다를 게 없다고 비판하는 사람들도 있다.
 반면에 Try 모나드를 사용한 예외처리는 좀 더 예측할 수 있고 가독성 있는 코드를 작성할 수 있게 해준다.

 Try 모나드는 Option과 마찬가지로 두 모나드의 sum type이다. 하지만 하나의 타입 파라미터를 받는 Option과 다르게 타입 파라미터를 두 개 받는다. 이 두 타입은 각각 성공했을 때 결과 타입인 T와 에러가 발생했을 에러 타입인 E다. 그래서 Try 모나드는 Try[T, E]로 표현한다. Try 모나드가 가지는 타입은 각각 실행이 정상적으로 되어 T 타입의 값을 가지는 경우인 Ok[T]와 에러가 발생하여 E 타입의 에러가 난 경우인 Error[E]이다.

 Try는 두 개의 타입 파라미터를 가지고 있는 만큼 타입을 진행시키는 bind 함수도 두 개 있다.

 첫 번째 bind 함수는 Try[T, E] 타입을 Try[U, E]로 진행시킨다. 이 함수는 현재의 TryOk일 경우에만 실행된다. 현재의 값을 콜백 함수에 넣어 실행시키며, 콜백 함수의 결과가 이 bind 함수의 결과가 된다. 에러가 발생한 Try인 Error일 경우 콜백 함수를 실행시키지 않으며, 결괏값은 현재와 같은 Error 타입이 된다.

 두 번째 bind 함수 Try[T, E] 타입을 Try[T, F]로 진행시킨다. 이 함수는 위의 함수와 반대로 현재 값이 Ok일 경우 아무 일도 하지 않고, Ok인 값을 그대로 반환한다. 반면에 현재 TryError일 경우, 에러값을 콜백 함수에 넣어 그 결괏값을 반환한다.

 이처럼 Try 모나드는 정상적인 실행 결과를 가지거나 에러값을 가진다. 이를 서로 다른 bind 함수를 이용하여 진행시킨다. 따라서 컨트롤 플로우가 뛰지 않고 언제나 bind 함수에 넘긴 콜백을 실행시키는 것으로 진행된다. 또한, 에러가 타입으로 나오기 때문에 어떤 함수가 어떤 에러를 발생시킬지 쉽게 알 수 있고, 발생한 에러가 어떻게 처리되는지를 컴파일 타임에 알 수 있다.