2. Exception-Safety Issues and Techniques

8. Writing Exception-Safe Code-Part 1

다음 코드는 예외로부터 안전한가?

template <class T> class Stack 
{
public:
 Stack();
 ~Stack();

   /*...*/

private:
  T*     v_;      // ptr to a memory area big
  size_t vsize_;  //  enough for 'vsize_' T's
  size_t vused_;  // # of T's actually in use
};

// Is this safe? 

template<class T> 
Stack<T>::Stack()
  : v_(new T[10]),  // default allocation
    vsize_(10),
    vused_(0)       // nothing used yet
{
}

생성자는 안전하다. 예외를 catch하지 않으니 예외는 호출자에 전파된다. 또한 누수도 없으며, 할당은 원자적이다. 예외가 있더라도 자원이 올바르게 해제되고 데이터가 일관성 있는 상태에 있도록 해야 한다. 소멸자는 다음과 같다.

template<class T> 
Stack<T>::~Stack()
{
  delete[] v_;      // this can't throw
}

여기서는 예외가 던져지지 않는다. operator delete나 operator delete[]는 절대 예외를 던져서는 안 된다. 소멸자에서 예외가 던져져도 안 된다. 모든 소멸자나 할당해제 함수는 noexcept를 쓰라.

9. Writing Exception-Safe Code-Part 2

Stack의 복사 생성자와 복사 대입 연산자를 예외 안전하게 써 보자. 다음과 같다.

template<class T> 
T* NewCopy( const T* src,
            size_t   srcsize,
            size_t   destsize )
{
  assert( destsize >= srcsize );
  T* dest = new T[destsize];
  try
  {
    copy( src, src+srcsize, dest );
  }
  catch(...)
  {
    delete[] dest; // this can't throw
    throw;         // rethrow original exception
  }
  return dest;
}

template<class T> 
Stack<T>::Stack( const Stack<T>& other )
  : v_(NewCopy( other.v_,
                other.vsize_,
                other.vsize_ )),
    vsize_(other.vsize_),
    vused_(other.vused_)
{
}

template<class T> 
Stack<T>&
Stack<T>::operator=( const Stack<T>& other )
{
  if( this != &other )
  {
    T* v_new = NewCopy( other.v_,
                        other.vsize_,
                        other.vsize_ );
    delete[] v_;  // this can't throw
    v_ = v_new;   // take ownership
    vsize_ = other.vsize_;
    vused_ = other.vused_;
  }
  return *this;   // safe, no copy involved
}

각 함수에서, 예외를 던질 수 있는 모든 코드들에 관련된 작업들은 모아놓고 하자. 이제 프로그램의 상태를 수정하는 것은 예외를 던지지 않는 연산들에서만 하자.

10. Writing Exception-Safe Code-Part 3

이제 Count(), Push(), Pop()을 예외 안전하게 써 보자. 다음과 같다.

template<class T> 
size_t Stack<T>::Count() const
{
  return vused_;  // safe, builtins don't throw
}

template<class T> 
void Stack<T>::Push( const T& t )
{
  if( vused_ == vsize_ )  // grow if necessary
  {                       // by some grow factor
    size_t vsize_new = vsize_*2+1;
    T* v_new = NewCopy( v_, vsize_, vsize_new );
    delete[] v_;  // this can't throw
    v_ = v_new;   // take ownership
    vsize_ = vsize_new;
  }
  v_[vused_] = t;
  ++vused_;
}

// Hmmm... how safe is it really? 

template<class T>
T Stack<T>::Pop()
{
  if( vused_ == 0)
  {
    throw "pop from empty stack";
  }
  else
  {
    T result = v_[vused_-1];
    --vused_;
    return result;
  }
}

다시 강조한다. 각 함수에서, 예외를 던질 수 있는 모든 코드들에 관련된 작업들은 모아놓고 하자. 이제 프로그램의 상태를 수정하는 것은 예외를 던지지 않는 연산들에서만 하자. 예외 안전성을 절대 이후에 생각하지 말자. 그건 클래스 설계이다. 절대 단순한 세부 구현이 아니다. Pop()은 단일 함수 단일 책임의 역할을 다하지 못하고 있기 때문에 둘을 분리하는 게 낫다:

template<class T> 
T& Stack<T>::Top()
{
  if( vused_ == 0)
  {
    throw "empty stack";
  }
  return v_[vused_-1];
}

template<class T>
void Stack<T>::Pop()
{
  if( vused_ == 0)
  {
    throw "pop from empty stack";
  }
  else
  {
    --vused_;
  }
}

응집을 선호하라. 코드 부분-각 모듈, 각 클래스, 각 함수-가 단일의 잘 정의된 책임을 다하도록 하라. 예외 불안전과 나쁜 설계는 같이 간다. 코드 일부분이 예외에서 불안전하면 그것은 일반적으로 큰 문제는 없고 간단히 고치면 된다. 그러나 기반 설계 때문에 코드가 예외 안전해질 수 없다면 그것은 거의 항상 나쁜 설계의 신호이다. 예 1. 서로 다른 두 책임의 함수는 예외 안전하게 만들기 힘들다. 예 2. 복사 대입 연산자가 복사 대입을 항상 체크하도록 쓰여 있다면 강한 예외 안전이 아닐 것이다.

11. Writing Exception-Safe Code-Part 4

예외에 대한 3종류의 보장은 다음과 같다.

  • T에 의해 던져진 예외나 다른 예외들이 존재하더라도, Stack 오브젝트는 자원을 누수하지 않는다.
  • 예외로 인해 동작이 중지되더라도, 프로그램 상태는 불변일 것이다.
  • 아예 예외를 던지지 않는다.

이 3종류의 보장을 이해해야 한다.

12. Writing Exception-Safe Code-Part 5

Stack을 꼭 필요한 만큼만 원소를 담은 StackImpl로 바꿔보자. 생성자와 소멸자는 다음과 같다.

// construct() constructs a new object in 
// a given location using an initial value
//
template <class T1, class T2>
void construct( T1* p, const T2& value )
{
  new (p) T1(value);
}
// destroy() destroys an object or a range 
// of objects
//
template <class T>
void destroy( T* p )
{
  p->~T();
}
template <class FwdIter>
void destroy( FwdIter first, FwdIter last )
{
  while( first != last )
  {
    destroy( &*first );
    ++first;
  }
}
// swap() just exchanges two values
//
template <class T>
void swap( T& a, T& b )
{
  T temp(a); a = b; b = temp;
}

template <class T> 
StackImpl<T>::StackImpl( size_t size )
  : v_( static_cast<T*>
          ( size == 0
            ? 0
            : operator new(sizeof(T)*size) ) ),
    vsize_(size),
    vused_(0)
{
}

template <class T> 
StackImpl<T>::~StackImpl()
{
    destroy( v_, v_+vused_ ); // this can't throw
    operator delete( v_ );
}

스왑 함수는 다음과 같다.

template <class T> 
void StackImpl<T>::Swap(StackImpl& other) throw()
{
    swap( v_,     other.v_ );
    swap( vsize_, other.vsize_ );
    swap( vused_, other.vused_ );
}

응집을 선호하라. 코드 부분-각 모듈, 각 클래스, 각 함수-가 단일의 잘 정의된 책임을 다하도록 하라. 기반 클래스의 멤버 변수는 protected, public으로 쓸 수 있는데 public이 낫다. protected 멤버는 의미가 없다.

13. Writing Exception-Safe Code-Part 6

이제 StackImpl을 기반으로 한 Stack의 API를 구현해 보자.

template <class T> 
class Stack : private StackImpl<T>
{
public:
  Stack(size_t size=0)
    : StackImpl<T>(size)
  {
  }

Stack(const Stack& other) 
  : StackImpl<T>(other.vused_)
{
  while( vused_ < other.vused_ )
  {
    construct( v_+vused_, other.v_[vused_] );
    ++vused_;
  }
}

Stack& operator=(const Stack& other) 
{
  Stack temp(other); // does all the work
  Swap( temp );      // this can't throw
  return *this;
}

Stack& operator=(Stack temp) 
{
  Swap( temp );
  return *this;
}

size_t Count() const 
{
  return vused_;
}

void Push( const T& t ) 
{
  if( vused_ == vsize_ )  // grow if necessary
  {
    Stack temp( vsize_*2+1 );
    while( temp.Count() < vused_ )
    {
      temp.Push( v_[temp.Count()] );
    }
    temp.Push( t );
    Swap( temp );
  }
  else
  {
    construct( v_+vused_, t );
    ++vused_;
  }
}

T& Top() 
{
  if( vused_ == 0 )
  {
    throw "empty stack";
  }
  return v_[vused_-1];
}

void Pop() 
{
  if( vused_ == 0 )
    {
      throw "pop from empty stack";
    }
    else
    {
      --vused_;
      destroy( v_+vused_ );
    }
  }
};

기본적인 예외 안전 규칙인 “자원 획득은 초기화” 관용구를 통해 자원 소유와 관리를 분리시켜라. 각 함수에서, 예외를 던질 수 있는 모든 코드들에 관련된 작업들은 모아놓고 하자. 이제 프로그램의 상태를 수정하는 것은 예외를 던지지 않는 연산들에서만 하자.

14. Writing Exception-Safe Code-Part 7

StackImpl의 접근자가 public이면 어떻게 될까? 다음과 같다.

template <class T> 
class Stack
{
public:
  Stack(size_t size=0)
    : impl_(size)
  {
  }

  Stack(const Stack& other)
    : impl_(other.impl_.vused_)
  {
    while( impl_.vused_ < other.impl_.vused_ )
    {
      construct( impl_.v_+impl_.vused_,
                 other.impl_.v_[impl_.vused_] );
      ++impl_.vused_;
    }
  }

  Stack& operator=(const Stack& other)
  {
    Stack temp(other);
    impl_.Swap(temp.impl_); // this can't throw
    return *this;
  }

  size_t Count() const
  {
    return impl_.vused_;
  }

  void Push( const T& t )
  {
    if( impl_.vused_ == impl_.vsize_ )
    {
      Stack temp( impl_.vsize_*2+1 );
      while( temp.Count() < impl_.vused_ )
      {
        temp.Push( impl_.v_[temp.Count()] );
      }
      temp.Push( t );
      impl_.Swap( temp.impl_ );
    }
    else
    {
      construct( impl_.v_+impl_.vused_, t );
      ++impl_.vused_;
    }
  }

  T& Top()
  {
    if( impl_.vused_ == 0 )
    {
      throw "empty stack";
    }
    return impl_.v_[impl_.vused_-1];
  }

  void Pop()
  {
    if( impl_.vused_ == 0 )
    {
      throw "pop from empty stack";
    }
    else
    {
      --impl_.vused_;
      destroy( impl_.v_+impl_.vused_ );
    }
  }

private:
  StackImpl<T> impl_; // private implementation
};

15. Writing Exception-Safe Code-Part 8

StackImpl을 private 기반 클래스로 쓰는 것이 좋은가 멤버 오브젝트로 쓰는 것이 좋은가? Stack의 마지막 두 버전은 얼마나 재사용성이 있는가? Stack은 함수에 예외 명세를 해야하는가?

  • private 기반 클래스나 멤버 오브젝트는 비슷하지만, private 기반 클래스는 클래스의 protected 멤버에 접근해야 할 때, 가상 함수를 오버라이드 해야 할 때, 오브젝트가 기반 부분오브젝트 이전에 생성되어야 할 때 쓰자. 아니면 멤버 오브젝트로 쓰자.
  • 이들은 오브젝트 생성/소멸과 메모리 관리를 분리시킨다. 또한, 각 오브젝트를 그 자리에서 필요할 때 생성하고 파괴하기 때문에 포함된 타입 T에 대해 더 나은 효율성을 보인다. 설계는 재사용성을 염두에 두고 하라.
  • Stack은 컨테이너가 포함하는 타입 T가 무슨 예외를 던질 지 알 수 없기 때문에 예외 명세를 할 수는 없다.

16. Writing Exception-Safe Code-Part 9

예외를 던질 수 있는 소멸자는 악이다.

template <class FwdIter> 
void destroy( FwdIter first, FwdIter last )
{
  while( first != last )
  {
    destroy( &*first ); // calls "*first"'s destructor
    ++first;
  }
}

T의 소멸자가 예외를 던질 수 있기 때문에 이는 문제가 된다. 다음의 접근이 가능하다.

template <class FwdIter> 
void destroy( FwdIter first, FwdIter last )
{
  while( first != last )
  {
    try
    {
      destroy( &*first );
    }
    catch(...)
    {
      /* what goes here? */
    }
    ++first;
  }
}

여기서 catch는 예외를 던지면 안 된다. 절대 소멸자에서 예외가 빠져나오게 하지 마라. operator delete나 operator delete[]는 절대 예외를 던져서는 안 된다. 소멸자에서 예외가 던져져도 안 된다. 모든 소멸자나 할당해제 함수는 noexcept를 쓰라. “자원 획득은 초기화” 관용구를 통해 자원 소유와 관리를 분리시켜라. 각 함수에서, 예외를 던질 수 있는 모든 코드들에 관련된 작업들은 모아놓고 하자. 이제 프로그램의 상태를 수정하는 것은 예외를 던지지 않는 연산들에서만 하자.

17. Writing Exception-Safe Code-Part 10

C++ 표준 라이브러리는 예외 안전한가? 그렇다. 모든 반복자는 예외를 던지지 않는다. STL 컨테이너의 생성/소멸 시 T에 의해 던져진 예외나 다른 예외들이 존재하더라도 자원을 누수하지 않는다. swap 등의 중요 함수는 noexcept 보장을 한다. 모든 STL 컨테이너는 연산에 있어 원자적이다. 하지만 반복자 범위 삽입 등은 예외로부터 안전하지 않다. std::vector<std::string> 등에 대한 삽입/삭제 등도 그렇다.

18. Code Complexity-Part 1

다음 코드에 얼마나 많은 실행 경로가 있는가?

String EvaluateSalaryAndReturnName( Employee e ) 
{
  if( e.Title() == "CEO" || e.Salary() > 100000 )
  {
    cout << e.First() << " " << e.Last() << " is overpaid" << endl;
  }
  return e.First() + " " + e.Last();
}

정답은 23가지이다. 임시 오브젝트의 생성이나 operator<< 등에서 예외가 던져질 수 있기 때문이다.

19. Code Complexity-Part 2

위의 코드를 예외 안전하게 만들어 보자. 정답은 다음과 같다.

// Attempt #3: Correct (finally!). 
//
unique_ptr<String>
EvaluateSalaryAndReturnName( Employee e )
{
  unique_ptr<String> result
      = make_unique<String>( e.First() + " " + e.Last() );

  if( e.Title() == "CEO" || e.Salary() > 100000 )
  {
    String message = (*result) + " is overpaid\n";
    cout << message;
  }

  return result;  // rely on transfer of ownership;
                  // this can't throw
}

3종류의 예외 보장을 이해해야 한다. 응집을 선호하라. 코드 부분-각 모듈, 각 클래스, 각 함수-가 단일의 잘 정의된 책임을 다하도록 하라.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중