이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 201p ~ 210p
이펙티브 C++(201p ~ 210p)
요약
항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
- 다음 클래스는 스레딩 환경에서 동작할 수 있게 설계된 GUI 메뉴 클래스이다.
class PrettyMenu {
public:
// 배경을 변경하는 함수
void changeBackground(std::istream& imgSrc);
private:
Mutex mutex;
// 현재 배경
Image* bgImage;
// 변경된 횟수
int imageChanges
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(mutex);
}
- 예외 안전성 측면에서는 나쁜 함수이다.
- 예외 안전성을 확보하기 위해 두 가지의 요구사항을 충족시켜야 한다.
- 자원이 새도록 만들지 않는다. 위의 코드는 자원이 샌다.
new
연산자를 사용하는 도중에 예외가 발생하면 unlock 함수가 호출되지 않아 mutex가 락된 상태로 남는다. - 자료구조가 더렵혀지는 것을 허용하지 않는다. 위의 코드는
new
연산자를 사용하는 도중에 예외가 발생하면 bgImage가 이미 삭제된 상태로 종료된다.
- 자원이 새도록 만들지 않는다. 위의 코드는 자원이 샌다.
- 자원 관리를 위한 객체를 이용해보자. 이를 통해 자원 누출 문제는 해결할 수 있다.
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
// 항목 14 참고
Lock m1(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
- 자료구조 오염을 해결하기 위한 용어 공부가 필요하다.
- 기본적인 보장: 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장이다. 모든 클래스 불변속성이 만족된 상태이다. 하지만 프로그램의 상태를 정확히 예측할 수 없을 수 있다.
- 강력한 보장: 함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다. 이런 함수를 호출하는 것은 원자적인 동작이라 할 수 있다.
- 예외불가 보장: 예외를 절대로 던지지 않겠다는 보장이다. 약속한 동작은 언제나 끝까지 완수하는 함수라는 뜻이다.
- 예외 안전성을 갖춘 함수는 세 가지 보장 중 하나를 반드시 제공해야 한다.
- 실용성의 측면에서는 강력한 보장이 괜찮다.
- 예외 안전성 측면에서는 예외불가 보장이 괜찮다.
- 그러나 실질적으로 예외를 던지는 함수를 호출하지 않고 C++의 C 부분으로부터 벗어나오긴 힘들다.
- 현실적으로는 대부분의 함수에 있어서 기본적인 보장과 강력한 보장 중 하나를 고른다.
// 강력한 보장
class PrettyMenu {
public:
// 배경을 변경하는 함수
void changeBackground(std::istream& imgSrc);
private:
// 포인터 대신 자원관리 객체 사용
std::tr1::shared_ptr<Image> bgImageData;
};
// 문장 재배치
// Image 객체를 프로그래머가 직접 삭제하지 않음
// 새로운 배경그림이 제대로 만들어졌을 때만 이전 배경그림의 삭제 작업이 진행됨
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
// reset 함수가 호출되려면 new Image가 정상적으로 생성되어야 함
// delete 연산자는 reset 함수 안으로 들어감
// 따라서 reset이 불리지 않으면 delete도 사용될 일이 없음
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
- 위의 코드에서 옥의 티는 매개변수인
imgSrc
에 대한 처리를 할 때이다.Image
클래스의 생성자가 실행되다가 예외를 일으킬 때, 그 시점에 입력 스트림의 읽기 표시자가 이동한 채로 남아 있을 가능성이 있다.- 따라서 엄밀히 말하면 위의 함수는 기본적인 보장이라고 할 수 있지만, 일단은 넘어간다.
예외 안전성 보장을 위한 설계 전략
- 복사 후 맞바꾸기라는 이름으로 알려져 있다.
- 어떤 객체를 수정하고 싶으면 그 객체의 사본을 하나 만들고, 사본을 수정하는 것이다.
- 이러면 사본 객체 수정 중에 예외가 발생해도 원본 객체는 바뀌지 않는다.
- 수정 동작이 모두 완료되면 수정된 객체를 원본 객체와 맞바꾼다.
- 이 작업을 예외를 던지지 않는 연산에서 처리한다.
struct PMImpl {
std::tr1::shared_ptr<Image> bgImage;
int imageChange;
}
class PrettyMenu {
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
public:
void changeBackground(std::istream& imgSrc) {
// 항목 25 참고
using std::swap;
Lock ml(&mutex);
// 객체 데이터 부분 복사
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
// 사본 수정
pNew->bgImage.reset(new Image(imgSrc));
// 맞바꾸기
swap(pImpl, pNew);
} // release mutex
}
- 복사 후 맞바꾸기 전략은 객체의 상태를 전부 바꾸거나 혹은 안 바꾸거나 방식으로 유지하려는 경우에 적합하다.
- 그러나 함수 전체가 강력한 예외 안전성을 갖도록 보장하지는 않는다.
void changeBackground(std::istream& imgSrc) {
using std::swap;
Lock ml(&mutex);
// 객체 데이터 부분 복사
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
// 사본 수정 단계
// 사본 수정
pNew->bgImage.reset(new Image(imgSrc));
// ...
// 여기에서 호출되는 함수가 예외 안전성이 없을 수 있음
// 사본 수정 단계 종료
// 맞바꾸기
swap(pImpl, pNew);
}
- 복사 후 맞바꾸기는 중간 단계에서 호출되는 함수가 예외 안전성이 강력하지 못하면, 전체 함수가 강력한 예외 안전성을 보장하기 힘들다.
- 강력한 보장을 제공하게 만들려면 중간 함수를 호출하기 전에 프로그램 전체의 상태를 결정하고, 중간 함수에서 발생하는 모든 예외를 잡아낸 후에 원래의 상태로 돌아가는 코드를 작성해야 한다.
- 그러나 중간 함수가 여러개 존재하면 사이드 이펙트를 무시할 수 없게 된다.
- 또한, 복사 후 맞바꾸기 전략은 시간과 공간적인 측면에서도 무시할 수 없는 비용이 든다.
- 강력한 예외 안전성 보장이 힘들면 기본적인 보장으로 눈을 돌려도 된다.
메모
항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
- 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도
자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않는다.
이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다. - 강력한 예외 안전성 보장은
복사 후 맞바꾸기
전략을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다. - 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(230p ~ 239p) (0) | 2024.01.29 |
---|---|
이펙티브 C++(210p ~ 220p) (2) | 2024.01.26 |
이펙티브 C++(191p ~ 200p) (0) | 2024.01.25 |
이펙티브 C++(183p ~ 190p) (1) | 2024.01.23 |
이펙티브 C++(169p ~ 182p) (2) | 2024.01.22 |