Effective C++/Effective C++
[Effective C++] 29. 예외 안전성
김디트
2024. 6. 25. 11:36
728x90
항목 29 : 예외 안전성이 확보되는 그날을 위해 싸우고 또 싸우자!
예외 안정성을 고려하지 않은 코드
class PrettyMenu
{
public:
...
void changeBackground(istream& imgSrc); // 배경 그림을 바꾸는 함수
...
private:
Mutex mutex;
Image* bgImage;
int imageChanges;
};
void PrettyMenu::changeBackground(istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
예외 안전성을 확보하기 위한 두 가지 요구사항
- 자원이 새도록 만들지 않는다.
- 하지만 위의 코드는 자원이 샌다.
- "new Image(imgSrc)" 표현식에서 예외를 던지면?
- unlock 함수가 실행되지 않게 되어 뮤텍스가 계속 잡힌 상태로 남게 된다.
- 해결법
- 자원 관리 객체를 사용한다.(항목 14 참조)
-
{ Lock ml(&mutex); // 유효 범위 내에서만 동작하는 자원 관리 객체 사용 delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); }
- 자료구조가 더럽혀지는 것을 허용하지 않는다.
- 자료구조가 의도한 바를 거스른다.
- "new Image(imgSrc)" 표현식에서 예외를 던지면?
- bgImage가 가리키는 객체는 이미 삭제된 후이다.(쓰레기 포인터가 된다.)
- bgImage가 invalid함에도 imageChanges변수가 증가한다.
예외 안전성을 갖춘 함수가 선택할 수 있는 세 가지 보장
- 기본적인 보장(basic guarantee)
- 함수 동작 중 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다고 보장한다.
- 모든 객체 상태는 일관성을 유지한다.(즉, 모든 클래스 불변속성이 만족된 상태이다.)
- 하지만 프로그램 상태를 예측하는 건 불가할 수 있다.
- 예컨대, changeBackground 함수 동작 중 예외가 발생했을 경우.
- PrettyMenu 객체는 이전 배경그림을 그대로 계속 그릴 수도 있고, 아니면 초기값 기본 배경그림을 사용할 수도 있을 것이다.
- 즉, 유효성이 보장되긴 하나 결과 예측이 불가능하다.
- 강력한 보장(strong guarantee)
- 함수 동작 중 예외가 발생하면, 프로그램 상태를 절대로 변경하지 않겠다고 보장한다.
- 호출 성공 시 마무리까지 완벽히 성공하지만, 실패 시 호출이 없었던 것처럼 프로그램 상태가 되돌아간다.
- 예외 불가 보장(nothrow guarantee)
- 예외를 절대로 던지지 않겠다고 보장한다.
- 약속한 동작은 언제나 끝까지 완수한다.
- 기본 제공 타입(int, 포인터 등)에 대한 모든 연산은 예외를 던지지 않도록 되어있다.
- 어떤 예외도 던지지 않게 예외 지정이 된 함수는 예외 불가 보장을 제공한다고 생각하는 것은 잘못된 생각이다.
-
int doSomething() throw(); // 비어있는 예외 지정 // 모던 c++에서는 noexcept 로 대체되었다.
- 이 경우 doSomething은 절대 예외를 던지지 않겠다는 말이 아니디.
- 만약 예외가 발생하면 매우 심각한 에러이므로 unexpected 함수(지정되지 않은 예외 발생 시 실행되는 처리자)가 호출되어야 한다는 뜻이다.
- 함수가 어떤 특성을 갖느냐는 구현이 결정한다. 선언은 그저 선거 공약 같은 것이다.
-
- 예외 불가 보장이 좋겠지만, 현실적으로는 대부분 기본적인 보장 혹은 강력한 보장 중 한 가지를 선택하게 된다.
위 코드도 예외 시 자료구조를 더럽히지 않도록 해결해보자
-
class PrettyMenu { ... shared_ptr<Image> bgImage; ... }; void PrettyMenu::changeBackground(istream& imgSrc) { Lock ml(&mutex); bgImage.reset(new Image(imgSrc)); // 자원관리 포인터로 바꾸어 예외 처리를 전담시킨다. // 이로써 자원 할당 중 예외가 발생해도 bgImage는 이전 값이 유지된다.(강력한 보장) ++imageChanges; // bgImage가 실제 변경된 후로 옮겨 자료구조를 보장해준다. }
- 강력한 보장처럼 보이지만 아직까지는 기본적인 보장에 불과하다.
- 매개변수 imgSrc가 'new Image(imgSrc)'에서 예외가 발생할 시, 입력 스트림의 읽기 표시자가 이동한 채로 남아 있을 가능성이 충분히 있다.
- 다양한 방법으로 보완이 가능할 것이다.
- imgSrc를 값복사하여 전달한다.
- img의 path를 전달해서 Image 생성자에서 istream을 생성한다.
복사 후 맞바꾸기(copy-andswap) 전략
- '강력한 보장'을 쉽게 제공하는 전략이다.
- 객체를 수정하고 싶을 때, 그 객체의 사본을 하나 만들어 놓고 그 사본을 수정한다.
- pimpl 관용구를 사용하여 구현하는 것이 일반적이다.
-
struct PMImpl { shared_ptr bgImage; int imageChanges; }; class PrettyMenu { ... private: Mutex mutex; shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground(istream& imgSrc) { using std::swap; Lock ml(&mutex); shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // pImpl을 복사한다. // 복사 객체에 대해 작업한다. pNew->bgImage.reset(new Image(imgSrc)); ++pNew->imageChanges; swap(pImpl, pNew); // 스왑한다. }
- 전부 바꾸거나 혹은 안 바꾸거나(all-or-nothing) 방식으로 유지하기 수월하다.
- 하지만, 함수 전체를 '강력한 보장'이라 하긴 힘들다.
- 예를 들자면 아래의 경우 '강력한 보장'이 아니다.
-
void someFunc() { .... // 이 함수의 현재 상태를 사본으로 한다. f1(); f2(); // f1(), f2()의 예외 안전성이 강력하지 않으면 // someFunc() 역시 강력한 예외 안전성을 제공한다 볼 수 없다. .... // 변경된 상태를 바꾸어 넣는다. }
- 문제점
- f1, f2가 강력한 예외 안정성을 지원한다는 보장이 없다.
- f1, f2가 강력한 예외 안정성을 보장한다 쳐도
- f1이 성공한 후
- f2이 예외를 던짐
- 의 상황이라면 f1에 의해 이미 변화한 상태를 가지므로, someFunc는 강력한 보장이라 할 수 없다.
- 대다수의 함수에 있어 무리 없는 선택을 한다면 기본적인 보장이 우선이다.
- 어떤 함수가 제공하는 예외 안정성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.
- 예외에 안전하거나, 예외에 뚫려 있거나 둘 중 하나이다. 일부만 예외 안전성을 갖춘 시스템 같은 것은 없다.
728x90