본문 바로가기

Effective C++/5. 구현

항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!

 

 

예외 안정성에 이보다 나쁜 함수가 없다.

예외 안정성을 가진 함수를 만들려면...

 

  1. 자원이 새도록 만들지 않는다.

 

코드는 만약 new 에서 예외를 던지면 unlock() 실행되지 않기 때문에 뮤텍스가 계속 lock 상태로 남게 된다.

 

  1. 자료구조가 더럽혀지는 하용하지 않는다.

 

코드는 만약 new 에서 예외를 던지면 bgImage 가르키는 객체는 이미 사라진 후이고, 그림이 깔리지도 않았는데 imageChanges 1 증가하고, 여튼 문제가 많다.

 

======================================================================

 

자원 누출 문제부터 해결해보자. => 자원 관리 객체를 사용하면

항목 13 참고하자.

 

일단 자원(mutex) 새진 않게 되었다. 자원 관리 객체에 집어 넣었기 때문이다.

 

 

 

======================================================================

 

 

이제 자료구조 더럽혀지는 것을 막아야 한다.

=> 이와 관련되서 선택해야 것이 있다.

예외 안정성을 갖춘 함수는 아래의 가지 guarantee(보장) 하나를 제공한다. ( 아무것도 제공하지 않으면 예외 안정성이 없는 함수)

 

  • 기본적인 보장(basic guarantee)

 

함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장

 

?? 이게 먼뜻임 => 어떤 객체나 자료구조도 더럽혀지지 않고(의도에 맞지 않는 데이터 왜곡), 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다.(모든 클래스 불변속성이 만족된 상태)

 

아직도 모르겠다.

예를 들어보면, changeBackground() 동작하다 예외가 발생 -> PrettyMenu 객체는 바로 이전의 bgImage 유지하고 있을 수도 있고, 처음부터 마련해 기본 배경그림을 사용할 수도 있다.(이부분은 함수 만든 사람 마음) => 사용자 쪽에서는 어느 쪽인지 예측이 불가능(이때, 현재의 배경그림이 무엇인지를 알려주는 다른 멤버 함수를 호출하던지 해야 있음)

 

예외가 발생하면, 결과가 어떻게 될지는 모르지만, 일단 logical error run-time error 안남(, 유효한 상태이다.)

 

  • 강력한 보장(strong guarantee)

 

함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다.

 

=> 강력한 보장을 제공하는 함수를 호출하는 것은 atomic(원자적인) 동작이라고 있다.(??)

=> 호출이 성공(예외가 발생하지 않으면)하면, 마무리까지 완벽하게 성공하고, 호출이 실패(예외가 발생하면)

하면 함수 호출이 없었던 것처럼 프로그램의 상태가 돌아간다.

 

예외 발생 => 사용자 쪽에서 함수 호출 이전의 상태로 돌아갔다고 예측 가능

 

강력한 보장이 기본적인 보장보다 쓰기 편하긴 하다. 예측할 있는 프로그램 상태가 2개밖에 안되기 때문이다. <-> 기본적인 보장은 프로그램 상태를 예측할 없음(사용자 입장에서), 하지만 기본적인 보장도 예외로 인해 어떤 에러(logical, run-time) 나는 것은 아님

 

 

  • 예외불가 보장(nothrow guarantee)

 

예외를 절대로 던지지 않겠다는 보장이다.

 

약속된 동작은 언제나 끝까지 완수하는 함수이다. 기본제공타입에 대한 모든 연산은 예외 불가 보장이 제공된다. => 예외에 안전한 코드를 만들기 위한 가장 기본적인 전제

 

여기서, 어떤 예외도 던지지 않게 예외 지정을 함수 => 예외불가 보장 함수이다?? => 틀림

 

int doSomething() throw();

 

여기서 throw 인자가 비어있음 => 예외를 던지지 않겠다는 것이 아니라, 예외가 발생하면 매우 심각한 에러가 발생한 것으로 판단되므로, 지정되지 않은 예외가 발생했을 경우에 실행되는 처리자인 unexpected 함수가 호출되어야 한다는 뜻이다.

 

그럼 함수는 어떤 보장을 제공하는가? => 모름, 함수의 특성은 선언이 아니라 '구현' 결졍하는 것이다.

 

만약 함수가 절대로 예외를 던진다. => 함수에 noexcept 키워드 사용, 그럼 함수는 예외불가 보장을 제공한다고 명시할 있음 => 구현에 미스가 있어 실제론 예외를 발생시켜도, 예외가 처리되지 않음

 

수까락의 프로그래밍 이야기 (zum.com) <= throw() noexcept 차이( throw() 예외불가 보장이 안되고, noexcept 예외불가 보장이 되는가)

 

====================================================================

 

예외로 인한 자원 누출 => 자원관리 객체를 사용

예외로 인한 자원 오염 => 3가지 보장 하나 선택하여 함수 설계

 

3가지 보장

  • 실용성이 가장 높은 것은 '강력한 보장'
  • 예외안정성이 가장 높은 것은 '예외 불가 보장'(하지만 현실적으로 구현이 어려움)
  • 안되면 '기본적인 보장'이라도 제공하는 것이 좋다.

STL 컨테이너들만 봐도, 동적 할당 메모리를 사용 => 메모리를 확보할 없을 bad_alloc 예외를 던짐

 

=====================================================================

 

changeBackground() 강력한 보장을 거의 제공할 있음

일단 PrettyMenu bgImage 자원 관리용 포인터로 바꾼다.(Image* => std::shared_ptr<Image>), 그리고 함수 내의 문장을 재배치해서 배경그림이 진짜로 바뀌기 전에는 imageChanges 증가시키지 않도록 한다.

 

어떤 동작이 일어났는지를 나타내는 객체를 프로그램 내에서 쓰는 경우, 해당 동작이 실제로 일어날 까지 객체의 상태를 바꾸지 않는 편이 일반적으로 좋다.

 

실제로 손을 코드를 보자.

 

 

이젠 Image객체를 직접 delete 필요가 없다. 스마트 포인터가 알아서 해줌, 그리고 reset() 실행될려면 new 연산자가 예외 없이 동작해야, 자원이 완전히 생성되야 reset() 실행되기 때문에, new에서 예외가 발생하면 원래 bgImage 아무런 변화가 없음. lock 또한 자원 관리 객체에 의해 자동으로 unlock() 호출함

 

근데 거의!!! 이다. => 가지 문제가 여지가 있는데, 바로 Image 생성자의 인자인 imgSrc이다. 만약 Image 생성자에서 예외가 터지면, 물론 reset() 실행되지 않아서 PrettyMenu 객체는 함수 호출 이전의 상태를 유지하지만, istream imgSrc 입력스트림이다. 입력스트림의 읽기 표시자가 초기화 과정중 이동했다가(입력스트림의 데이터를 옮기던가 해서) 그대로 예외가 터져서 읽기 표시자가 이동한 채로 남겨질 있다. => 사용자가 예상 못함(읽기 표시자가 이동했을지, 안했을지) => 엄밀히 따지면 changeBackground() 기본적인 보장을 제공한다.

 

======================================================================

 

하지만 우리는 changeBackground() '강력한 보장' 제공한다고 가정하고 진행할 것이다. (위의 문제를 해결할 방법은 여러 개가 있다. 예를들어 istream 말고, 배경화면 파일의 이름을 나타내는 타입 같은 걸로 바꿔보자.)

 

이번에는 예외에 안전하지 않은 함수 => 예외에 안전한 함수로 만드는 매우 간단한 전략 하나를 소개한다.

바로 copy-and-swap 이다.

 

어떤 객체를 수정하고 싶으면, 사본을 만들어 거기에 수정 , 원본과 swap하는 전략이다. 여기서 수정 중에 예외가 발생 => swap 전이므로 원본은 무사하다. 그리고 swap '예외를 던지지 않는' 연산 내부에서 수행한다. swap 예외불가 보장을 제공한다고 가정함

 

전략은 보통 진짜 객체의 모든 데이터를 별도의 구현(implementaion) 객체에 넣어두고, 구현 객체를 가르키는 포인터를 진짜 객체가 물고 있게 하는 식으로 구현한다. (pimpl 관용구) 항목 31 참고

 

방법을 적용해보자.

 

 

PMImpl struct 선언했나? => 어짜피 PrettyMenu에서 PMImpl 관리하는 객체인 pImpl private이기 때문이다.

 

copy-and-swap 객체의 상태를 '전부 바꾸거나' 혹은 '아예 안바꾸거나로 유지할 매우 유용하다.('강력한 보장' 특성이 같다.)

 

=> 그래도 함수 전체가 '강력한 보장' 제공하는 것이 아니다. => ?

changeBackground() 전체 흐름을 추상화 하자면,

void someFunc{

// copy-

f1();

f2();

//and-swap

}

이렇게 복사 맞바꾸기 과정에서 두가지 함수 호출 흐름이 끼어있는데, someFunc() '강력한 보장' 제공할려면

  1. f1() 전에 프로그램 전체의 상태를 결정하고
  2. f1()에서 발생하는 모든 예외를 잡아낸 후에
  3. 원래의 상태로 되돌리는 코드를 작성해야 한다.

 

강력한 보장이 그런 것이기 때문이다.(아무 문제 없이 돌아가던가, 아예 함수 호출 이전 상태로 유지하던가 둘중 하나여야함) -> 3개를 만족한다고 해도 완벽한 의미의 '강력한 보장' 힘듬

 

왜냐하면 => f1() 실행되면 어찌됬던 프로그램의 상태는 바뀐다.(데이터가 수정됨) => f2() 실행되다 예외가 터지면 someFunc() 호출되기 전으로 돌아가기란 불가능하다. 이미 f1()에서 상태가 바뀌었기 때문이다.

 

=> 함수의 side effect 문제 원인이다.

======================================================================

 

changeBackground() 같이 자기 자신에만 국한된 것들(지역 객체) 상태를 바꾸며 동작하는 함수의 경우는 '강력한 보장' 제공하기 수월하다.

 

그러나 비지역 객체에 대해 side effect 주는 함수는 이렇게 하기가 힘들다. 예를 들어 f1 호출하고 나서 생기는 side effect로서 데이터베이스가 변경된다고 생각해보자. someFunc()쪽에서는 어떻게 손을 수가 없다. 이미 확정된 데이터베이스를 다시 되돌릴 있는 방법은 거의 없다. 데이터베이스의 변경사항을 다른 사용자가 이미 확인을 했으면 다시 되돌릴 없음

 

=> 실용성이 확보될 때만 '강력한 보장' 사용하자. 사실 왠만한 함수들은 '기본적인 보장' 제공해도 충분함.

 

다만 '기본적인 보장'이라도 챙기자. 예외 안전성 보장을 제공하지 않는건 안됨, 특히 자원을 다루는 함수의 경우 특히 그렇다.

 

 

  • 예외 안전성을 갖ㄴ 함수는 실행 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않는다. 이런 함수들이 보장할 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외불가 보장이 있다.
  • 강력한 보장 copy-and-swap으로 쉽게 구현이 가능하지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.
  • 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.