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

Minsu._.Lighting·2023년 12월 6일
0

[ Effective C++ ] 정리 모음집
" C++ 프로그래머의 필독서, 스콧 마이어스의 Effective C++ 를 읽고 내용 요약 / 정리 "

[핵심]

" 예외 안전성을 확보하는 작업은 아기를 갖는 일만큼 힘들다! "

  • 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않는다.
  • 강력한 예외 안전성 보장은 '복사 후 맞바꾸기' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.
  • 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.

💡 예외 안전성

[ 예외 안전성을 확보하려면 ]

  • 자원이 새도록 만들지 않는다.
  • 자료구조가 더렵혀지는 것을 허용하지 않는다.

📌 예외 안전성 확보 실패 예시

[ 예시 코드 ]

class PrettyMenu
{
public:
	...
    void changeBackground(istream& imgSrc);
    ...
    
private:
	Mutex mutex;
    
    Image* bgImage;
    int imageChanges;
};



void PrettyMenu::changeBackground(istream& imgSrc)
{
	Lock(&mutex);
    
    delete bgImage;
    ++imageChanges;
    bgImagd = new Image(imgSrc);
    
    unlock(&mutex);
}
  • 위 코드는 자원 누출을 막지 못한다.
    - new Image(imgSrc) 표현식에서 예외가 발생하면 unlock 함수가 실행되지 않아 뮤텍스가 계속 잡혀있는 상태

  • 위 코드는 자료구조가 더렵혀지는걸 허용 함.
    - new Image(imgSrc) 표현식에서 예외가 발생하면 bgImage가 가리키는 객체는 이미 지워진 상태, 또 한 그림이 제대로 교체되지도 않았는데 imagechanges 변수는 이미 증가

📌 자원 누출 해결 방법

[ 예시 코드 ]

void PrettyMenu::changeBackground(istream& imgSrc)
{
	Lock ml(&mutex);
    
    delete bgImage;
    ++imageChanges;
    bgImagd = new Image(imgSrc);
}


💡 자료구조 오염문제 방지는 어떻게...?

  • 예외 안전성을 갖춘 함수는 기본적인 보장, 강력한 보장, 예외불가 보장 중 하나를 반드시 제공해야 함

📌 기본적인 보장(basic guarantee)

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

  • 어떤 객체나 자료구조도 더럽혀지지 않는다.

  • 모든 객체의 상태는 내부적으로 일관성을 유지

  • 프로그램의 상태가 정확히 어떠한지는 예측이 안 될 수도 있다.
    - 함수의 제작자는 알겠지만 사용자는 알 수 없음

📌 강력한 보장(strong guarantee)

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

  • 강력한 보장이 이루어진 함수를 호출하는 것은 원자적인(atomic) 동작
    - 예외가 발생하지 않으면 마무리까지 완벽하게 성공하고, 반대의 경우 함수가 아예 호출되지 않은 것 처럼 프로그램의 상태가 돌아간다는 면에서

  • 기본적인 보장 함수보다 강력한 보장이 이루어진 함수가 쓰기에 더 편하다.
    - 예측 가능한 프로그램의 상태가 두 개 뿐이기 때문
    - 기본적인 보장의 경우 프로그램이 있을 수 있는 상태가 유효하기만 하면 어떤 상태든 될 수 있음

📌 예외불가 보장(nothrow guarantee)

  • 예외를 절대로 발생시키지 않겠다는 보장
    - 약속한 동작은 언제나 끝까지 완수 하는...

  • 기본제공 타입(int, 포인터 등)에 대한 모든 연산은 예외를 발생시키지 않게 되어있다.

  • 예외에 안전한 코드를 만들기 위한 가장 기본적이며 핵심적인 요소

📢 어떤 예외도 발생시키지 않게끔 예외지정이 된 함수가 예외불가 보장을 제공한다고 생각하는 것은 잘못된 생각이다!
int doSomething() throw();
- doSomething이 절대로 예외를 발생시키지 않겠다는 것이 아닌 doSomething에서 예외 발생 시 매우 심각한 에러가 생긴것으로 판단, unexpected 함수가 호출 되어야 한다는 뜻
- 함수가 어떤 특성을 갖느냐 하는 부분은 '구현'이 결정하는 것



💡 셋중에 뭘 선택할까?

실용성이 있는 강력한 보장...?
예외 안전성 관점에서 봤을 때 가장 훌륭한 예외불가 보장...?

📢 현실적으로는 기본적인 보장과 강력한 보장 중 하나를 고르게 된다!
- 예외를 발생시키는 함수를 호출하지 않고 C++의 C부분으로 부터 벗어나오긴 쉽지 않기 때문


📌 강력한 보장 예시

[ 예시 코드 ]

class PrettyMenu
{
	...
    shared_ptr<Image> bgImage;
    ...
};

void PrettyMenu::changeBackground(istream& imgSrc)
{
	Lock ml(&mutex);
    
    bgImage.reset(new Image(imgSrc));
    
    ++imageChanges;
}
  • bgImage 데이터 멤버 타입을 기본제공 타입 Image* 에서 자원 관리 전담 포인터로 바꿔준다.
    - 자원 누출 해결

  • 함수 내 문장 재배치를 통해 배경 그림이 진짜로 바뀌기 전까지 imageChanges를 증가시키지 않도록 한다.
    - 자료구조의 오염도 막았다!

  • delete 연산자는 reset 함수 안에 들어가 있으므로, reset이 불리지 않는 한 delete도 쓰일 일이 없다!

📢 뭔가 부족해...

  • new Image가 예외를 발생시킬 때 입력 스트림의 읽기 표시자가 이동한 채로 남아있을 가능성이 있다
    - 엄밀히 따지자면 기본적인 보장이 되는 것...

📢 복사 후 맞바꾸기(copy and swap) 전략을 사용하면 강력한 예외 안전성을 보장할 수 있다!
[ 복사 후 맞바꾸기 예시 코드 ]

struct PMImpl
{
	shared_ptr<Image> bgImage;
    int imageChanges;
};

class PrettyMenu
{
...
private:
	Mutex mutex;
    shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(istream& imgSrc)
{
	using swap;
    
	Lock ml(&mutex);
    
    shared_ptr<PMImpl>pNew(new PMImpl(*pImpl));
    
    pNew->bgImage.reset(new Image(imgSrc));
    ++pNew->imageChanges;
    
    swap(pImpl, pNew);
}
  • 복사 후 맞바꾸기 전략은 객체의 상태를 '전부 바꾸거나 혹은 안바꾸거나(all-or-nothing)' 방식으로 유지하려는 경우 효과적이다

  • 하지만 함수 전체가 강력한 예외 안정성을 갖도록 보장하지는 않는다.
    [ 예시 코드 ]

void someFunc()
{
	...
    f1();
    f2();
    ...
}

- f1 혹은 f2에서 보장하는 예외 안전성이 '강력' 하지 못하면 someFunc 함수 역시 강력한 예외 안전성을 보장하기 힘들다.
- 어떤 함수가 제공하는 예외 안전성 보장 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 못한다.

profile
오코완~😤😤

0개의 댓글

관련 채용 정보