항목 29. 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자
예외에 대한 안전성을 갖춘 코드는 다음 항목을 지켜야한다.
1. 자원이 새도록 하지 않는다.
2. 자료구조가 더럽혀지는 것을 허용하지 않는다.
아래 예를 살펴보자.
class PrettyMenu {
public:
void changeBackground(istream& imgSrc);
private:
Mutex mutex;
Image *bgImage;
int imageChanges;
};
void PrettyMenu::changeBackground(istream& imgSrc)
{
lock(&mutex);
delete bgImage;
imageChanges++; // error 1
bgImage = new Image(imgSrc); // error 2
unlock(&mutex);
}
* 해설
- error2 에서 예외발생시 뮤텍스는 잠긴상태가 유지된다. -> 자원이 샌다.
- error2에서 bgImage는 삭제된 이미지를 가리킬 것이고, imageChanges는 작업이 완성되지 않았는데도 불구하고 1이 증가 되어있을것이다. --> error1
* 해결방법
- 우리는 13장에서 배운 자원관리 전담클래스 Lock을 사용해서 뮤텍스를 적절한 시점에 해결할 수 있도록 변경할 수 있다. -> 자원이 새지 않는다. 하지만 여전히, 자료구조는 망가진상태이다.
이를 해결하기 앞서 예외 안정성을 갖춘 함수가 제공하는 세가지 보장(guarantee)을 알아보자.
(3개중 최소 한 개를 제공해야 예외안전성이 완성된다.)
1. 기본적인 보장(basic guarantee) - 함수동작중 예외가 발생해도, 객체의 값은 유효하다.
2. 강력한 보장(strong guarantee) - 함수동작중 예외가 발생하면, 마치 함수호출이 없었던것처럼 한다. 호출이 성공하면 끝까지 예외없이 수행한다.
3. 예외불가 보장(nothrow gurantee) - 예외를 절대 던지지 않겠다는 보장이다. 기본타입형들이 예외불가 보장이된다.
3가지를 보아하니, 강력한 보장이 젤 나아보인다.
강력한 보장의 1번째 방식은, 13장에서 배웠던 자원관리 포인터(스마트포인터 등)를 사용해서 안전성 보장을 제공해보자. --> tr1::shared_ptr을 사용해보자.
2번째 방식은, 해당 동작이 실제 일어나기전까지 객체의 상태를 바꾸지 말자.
(ex. imageChanges를 객체 변경후 증가시키자)
class prettyMenu{
std::tr1::shared_ptr<Image> bgImage;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc)); // 변경
++imageChanges; // 객체실행후 증가하자
}
* 해결방법
이제 배경그림은 스마트포인터 통제하에 들어갔다. reset의 매개변수가 정상적으로 생성되어야만 하는 조건을 가진다.또한 delete도 reset이 제어한다. 자원관리가 효율적으로 변경된다. --> 강력한보장으로 거의 완벽해보인다.
하지만 이 방법은 "기본적 보장"이다. 왜냐하면 매개변수 imgSrc의 생성자호출 과정에서 에러발생시 문제를 일으키기 때문이다. 일단 이문제는 잠시 덮어두고 다음 챕터로 넘어가자.
이번엔 예외의 속수무책 함수를 강력한 예외 안전성을 보장하는 함수로 탈바꿈시켜보자.
--> " 복사 후 맞바꾸기 (copy-and-swap)" 방법
* copy-and-swap : '진짜' 객체의 모든 데이터를 별도의 구현(implementation)객체에 넣어두고 그 구현객체를 가리키는 포인터를 진짜 객체가 물고있게 한다.
즉, 연산에서 예외가 던져지더라도 원본객체는 바뀌지 않고, 바꾸는 작업도 '예외를 던지지 않는' 연산 내부에서 수행한다.(ex. std::swap)
struct PMImpl {
shared_ptr<Image> 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)); // 객체 데이터부분 복사한다.
pNew->bgImage.reset(new Image(imgSrc)); // 사본을수정 1
++pNew->imageChanges; // 사본수정2
swap(pImpl, pNew); // paste;
}
'copy - paste' 전략은 객체의 상태를 '전부 바꾸거나, 아에 안바꾸거나'방식으로 유지하는데 아주 좋지만, 그러나! 함수 전체가 강력한 예외보장을 갖도록 보장하지 않는다는 것이 정설이다.
예시를 살펴보자. (copy paste 수법을 쓰되, f1 및 r2라는 함수의 호출문이 들어 있는 형태이다.)
void someFunc()
{
… // 이 함수의 현재 상태에 대한 사본을 만들어 놓는다.
f1();
f2();
… // 변경된 상태를 바꾸어 놓는다.
}
만약 f1, f2가 예외 안전성이 강력하지 못하면, someFunc()도 강력하지 못하다.
그렇다고 f1, f2가 강력한 예외 안전성을 보장하더라도 상황은 나아지지 않는다.
만약 f1이 끝까지 실행되면 어쨌든 f1에 의해 뭔가(ex. 데이터베이스 데이터값) 변경되 어있을것이고 db값이라면, 돌이킬 수 없다. (사용자가 이미 확인해버렸을 수도 있으니)
또한 효율도 무시할 수 없다. 사본만들다 보니.. 복사공간과 소모시간을 감수해야한다. 어쨌든, 강력한 보장이 젤 좋긴한다. 안정성 측면에서는.
기본적인 보장 쪽으로 눈을 돌려보자. 강력한 보장을 접어야하는 상황에서 기본적인 보장을 제공한다고해서 뭐라고 할 사람이 없다.
c++ legacy코드는 예외 안전성자체를 고려하지 않고 만들어진게 많으니, 앞으로 새로운 함수를 만들거나, 기존의 코드를 고칠때에는 "어떻게 하면, 예외에 안전한 코드를 만들까"를 진지하게 고민하는 버릇을 들여야한다.
이것만은 잊지 말자
1. 예외 안전성을 갖춘 함수는 예외가 발생하더라도, 자원누출을 하지 않으며, 자료구조를 더럽히지 않는다. 이러한 함수들이 제공하는 안전성보장방법으로는 기본적, 강력한, 예외금지 보장이 있다.
2. 강력한 예외 안전성 보장은 'copy-and-swap' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인것은 아니다.
3. 어떤함수가 제공하는 안전성 보장의 정도는 그함수가 내부적으로 호출하는 함수들중 가장 약한 보장을 넘지 않는다.