C++ 이야기: 예외 처리와 관련된 고전 글 번역

Posted at 2010. 6. 30. 23:14 // in S/W개발/C++ 이야기 // by 김윤수


다음 글은 예외 처리 기능이 C++에 도입된지 얼마되지 않아 C++ 커뮤니티에서 예외를 사용할 때 유의해야할 점에 대해 잘 깨닫지 못하고 있을 때, 예외에 대한 새로운 통찰력을 제공해 주고, 예외 처리를 위한 다양한 기법들의 시발점이 된 고전 Tom Cargill 의 Exception handling: a false sense of security 를 번역한 것입니다. 원문은 이 링크에서 보실 수 있고, 원문에서 약간 코드 상 잘못된 부분들(template 키워드 뒤에 <class T>가 따라오지 않는다던지 literal string 이 ""..."" 과 같이 되어 있다던지)을 수정하고 영문과 한글을 대조하면서 보실 수 있도록 했습니다. C++ 로 프로그래밍 하시는 분이라면 필독해야 할 글이라고 할 수 있습니다.

그렇지만 이 글만 읽지 마시고, 2009/09/25 - [S/W개발/C++ 이야기] - C++ 이야기 서른한번째: 왜 예외를 쓰는 게 좋을까요? 와 비교해서 읽어 보시면 예외의 장점과 단점을 어느 정도 파악하실 수 있을 거라 생각합니다. 이 글만 읽으시면 마치 예외가 문제를 더 많이 일으키는 것 같지만, 이 글이 문제점을 제기한 이후에 많은 분들이 예외 안전성을 확보할 수 있는 프로그래밍 기법들을 알아내어 그 방법대로 프로그래밍하면 별 문제 될것이 없습니다. 다만, 이런 프로그래밍 기법을 공부하는데 어느 정도 시간이 필요한 것이 더 문제겠지요. 또 이 글만 읽고 예외는 절대 쓰면 안된다는 결론에 빠지지 않기를 바랍니다.

EXCEPTION HANDLING:
A FALSE SENSE OF SECURITY
예외 처리: 잘못된 안정감

Tom Cargill 저
김윤수 역

This article first appeared in C++ Report, Volume 6, Number 9, November-December 1994.
이 글은 C++ Report, 6권, 1994년 11월, 12월, 9번째판에 처음 게제됐습니다.

I suspect that most members of the C++ community vastly underestimate the skills needed to program with exceptions and therefore underestimate the true costs of their use. The popular belief is that exceptions provide a straightforward mechanism for adding reliable error handling to our programs. On the contrary, I see exceptions as a mechanism that may cause more ills than it cures. Without extraordinary care, the addition of exceptions to most software is likely to diminish overall reliability and impede the software development process.

저는 C++ 사용자 대부분이 예외를 사용하는 프로그램을 작성하는 데 필요한 노련미를 너무 무시하고 있지 않는지, 그럼으로 인해 예외를 사용할 때, 진정 치뤄야할 대가를 무시하고 있지는 않은지 염려스럽습니다. 오히려 대부분은 예외를 사용하면 바로 신뢰성 있는 에러 처리가 가능하다고 믿는 것 같습니다. 저는, 이와는 반대로, 예외를 사용하면 해결되는 문제보다는 새롭게 발생하는 문제가 많다고 생각합니다. 정말 정말 조심하지 않고 예외를 사용하면, 소프트웨어의 전반적인 신뢰성이 훼손되고 소프트웨어 개발 과정이 지연될 가능성이 높아집니다.

This "extraordinary care" demanded by exceptions originates in the subtle interactions among language features that can arise in exception handling. Counter-intuitively, the hard part of coding exceptions is not the explicit throws and catches. The really hard part of using exceptions is to write all the intervening code in such a way that an arbitrary exception can propagate from its throw site to its handler, arriving safely and without damaging other parts of the program along the way.

이렇게 정말 정말 조심해야 하는 이유는 예외 처리 과정에서 발생하는 언어의 다양한 특징들간 미묘한 상호 작용때문입니다. 언뜻 보기와는 달리 예외를 사용하여 코딩할 때, 가장 어려운 부분은 눈에 보이는 throw와 catch가 아닙니다. 정말 어려운 부분은 어떤 예외라도 발생 지점으로부터 처리 지점까지 프로그램 상태를 해치지 않고 안전하게 도달할 수 있도록 코딩하는 것입니다.

In the October 1993 issue of the C++ Report, David Reed argues in favor of exceptions that: "Robust reusable types require a robust error handling mechanism that can be used in a consistent way across different reusable class libraries." While entirely in favor of robust error handling, I have serious doubts that exceptions will engender software that is any more robust than that achieved by other means. I am concerned that exceptions will lull programmers into a false sense of security, believing that their code is handling errors when in reality the exceptions are actually compounding errors and hindering the software.

C++ Report 1993년 10월판에서 David Reed는 "강인한 재사용 형식은 여러 클래스 라이브러리에 걸쳐 일관되게 사용할 수 있는 강인한 오류 처리 방법을 필요로 한다"며 예외를 옹호하는 주장을 했습니다. 강인한 오류 처리가 필요하다는 건 십분 동의하지만, 다른 에러 처리 방법에 비해 예외가 더 강인한 소프트웨어를 만들게 한다는 건 정말 동의하기 어렵습니다. 오히려 프로그래머들이 예외를 사용하면서 내 프로그램은 오류 처리를 하고 있으니 안전할꺼야라며 안심하게 될까봐 걱정스럽습니다. 실제로는 예외가 오류를 더 복잡하게 하고, 소프트웨어 동작을 방해하게 됩니다.

To illustrate my concerns concretely I will examine the code that appeared in Reed's article. The code (page 42, October 1993) is a Stack class template. To reduce the size of Reed's code for presentation purposes, I have made two changes. First, instead of throwing Exception objects, my version simply throws literal character strings. The detailed encoding of the exception object is irrelevant for my purposes, because we will see no extraction of information from an exception object. Second, to avoid having to break long lines of source text, I have abbreviated the identifier current_index to top. Reed's code follows. Spend a few minutes studying it before reading on. Pay particular attention to its exception handling. [Hint: Look for any of the classic problems associated with delete, such as too few delete operations, too many delete operations or access to memory after its delete.]

제가 걱정하는 바를 구체적으로 설명하기 위해 Reed의 글에 있는 코드를 한 번 살펴 보겠습니다. 1993년 10월판 42페이지에 제시된 코드는 Stack 클래스 템플릿입니다. 설명을 위해 Reed의 코드를 줄일 필요가 있어서 두 가지를 수정했습니다. 첫 번째로 Exception 객체를 던지지 않고, 그냥 문자열을 던집니다. 어차피 예외 객체러부터 어떤 정보도 꺼내지 않을 것이므로 예외 객체를 어떻게 부호화하는지는 제 논의와 별 상관이 없습니다. 두 번째로 긴 소스 줄을 끊어야 하는 불편을 피하기 위해 current_index 식별자를 top이라고 줄였습니다. 다음은 Reed의 코드입니다. 계속 읽기 전에 한 번 몇 분간 깊이 분석해 보시기 바랍니다. 특히 예외 처리에 주목해 보시기 바랍니다. [힌트: delete 연산을 필요한 만큼 호출하지 않는다던지, 필요보다 더 많이 호출한다던지, delete 한 후에 메모리를 사용한다던지하는 delete와 관련된 고전적인 문제를 찾아 보시기 바랍니다.]

template <class T>
class Stack
{
  unsigned nelems;
  int top;
  T* v;
public:
  unsigned count();
  void push(T);
  T pop();

  Stack();
  ~Stack();
  Stack(const Stack&);
  Stack& operator=(const Stack&);
};

template <class T>
Stack::Stack()
{
  top = -1;
  v = new T[nelems=10];
  if( v == 0 )
    throw "out of memory";
}

template <class T>
Stack::Stack(const Stack& s)
{
  v = new T[nelems = s.nelems];
  if( v == 0 )
    throw "out of memory";
  if( s.top > -1 ){
    for(top = 0; top <= s.top; top++)
      v[top] = s.v[top];

    top--;
  }
}

template <class T>
Stack::~Stack()
{
  delete [] v;
}

template <class T>
void Stack::push(T element)
{
  top++;
  if( top == nelems-1 ){
    T* new_buffer = new T[nelems+=10];
    if( new_buffer == 0 )
      throw "out of memory";
    for(int i = 0; i < top; i++)
      new_buffer[i] = v[i];
    delete [] v;
    v = new_buffer;
  }
  v[top] = element;
}

template <class T>
T Stack::pop()
{
  if( top < 0 )
    throw "pop on empty stack";
  return v[top--];
}

template <class T>
unsigned Stack::count()
{
  return top+1;
}

template <class T>
Stack&
Stack::operator=(const Stack& s)
{
  delete [] v;
  v = new T[nelems=s.nelems];
  if( v == 0 )
    throw "out of memory";
  if( s.top > -1 ){
    for(top = 0; top <= s.top; top++)
      v[top] = s.v[top];
    top--;
  }
  return *this;
}

My examination of the code is in three phases. First, I study the code's behavior along its "normal," exception-free execution paths, those in which no exceptions are thrown. Second, I study the consequences of exceptions thrown explicitly by the member functions of Stack. Third, I study the consequences of exceptions thrown by the T objects that are manipulated by Stack. Of these three phases, it is unquestionably the third that involves the most demanding analysis.

저는 세 단계로 이 코드를 분석해 보려고 합니다. 우선, 정상적인, 즉, 예외가 없는 실행 경로를 따로 코드가 잘 동작하는지 분석할 것입니다. 그 다음에는 Stack의 멤버 함수가 예외를 던질 때 어떻게 되는지 분석할 것입니다. 마지막으로 Stack이 처리하는 T 객체가 예외를 던질 때 어떻게 되는지 분석할 것입니다. 당연히 이 세 단계 중 마지막 단계를 가장 철저히 분석해야겠지요.

Normal Execution Paths / 정상적 실행 경로

Consider the following code, which uses assignment to make a copy of an empty stack:

다음 코드는 비어 있는 스택 복사본을 만들기 위해 할당을 사용하고 있습니다:

Stack y;
Stack x = y;
assert( y.count() == 0 );
printf( "%u\n", x.count() );



17736

The object x should be made empty, since it is copied from an empty master. However, x is not empty according to x.count(); the value 17736 appears because x.topis not set by the copy constructor when copying an empty object. The test that suppresses the copy loop for an empty object also suppresses the setting of top. The value that top assumes is determined by the contents of its memory as left by the last occupant.

x는 비어 있는 원본에서 복사됐으니 당연히 비어 있어야 함에도 불구하고 x.count() 결과로 보면 비어 있질 않습니다; 빈 객체를 복사할 때 복사 생성자가 x.top을 설정하지 않기 때문에 17736이라는 값이 출력된 것입니다. 빈 객체에 대해서 복사 루프를 돌지 않게 하는 조건문 때문에 top 변수도 설정되지 않게 됩니다. top 변수의 값은 마지막으로 해당 메모리를 사용했던 객체가 남겨 놓은 값이겠지요.

Now consider a similar situation with respect to assignment:

할당과 관련된 비슷한 경우을 하나 더 생각해 보겠습니다:

Stack a, b;
a.push(0);
a = b;
printf( ""%u\n"", a.count() );



1

Again, the object a should be empty. Again, it isn't. The boundary condition fault seen in the copy constructor also appears in operator=, so the value of a.top is not set to the value of b.top. There is a second bug in operator=. It does nothing to protect itself against self-assignment, that is, where the left-hand and right-hand sides of the assignment are the same object. Such an assignment would cause operator= to attempt to access deleted memory, with undefined results.

여기에서도 객체 a는 비어 있어야 하지만 그렇지 않습니다. 경계 조건을 잘못 처리했던 복사 생성자의 결함이 operator=에도 있기 때문에 a.top 변수값이 b.top 변수값으로 설정되지 않습니다. operator=에는 버그가 또 하나 있습니다. 바로 자기 할당-좌변 객체와 우변 객체가 동일한 경우를 말함-을 별도로 처리하지 않고 있습니다. 자기 할당시에는 operator=가 delete된 메모리를 접근하게 되고, 그렇게 되면 예상치 못한 결과가 초래될 것입니다.

Exceptions Thrown by Stack / Stack 이 던지는 예외

There are five explicit throw sites in Stack: four report memory exhaustion from operator new, and one reports stack underflow on a pop operation. (Stack assumes that on memory exhaustion operator new returns a null pointer. However, some implementations of operator new throw an exception instead. I will probably address exceptions thrown by operator new in a later column.)

Stack은 다섯 군데에서 예외를 발생시키고 있습니다. 그 중 넷은 new 연산자가 메모리 고갈을 알려준 것을 보고하는 것이고, 나머지 하나는 pop 연산을 수행할 때 언더플로가 발생했다는 것을 보고하는 것입니다.(Stack 은 new 연산자가 메모리 고갈시 널 포인터를 리턴하는 것으로 가정하고 있습니다. 그렇지만 일부 컴파일러에서는 new 연산자가 예외를 던지는 방식으로 작동합니다. 나중에 쓰게 될 칼럼에서 new 연산자가 예외를 던지는 문제를 다뤄 보겠습니다.)

The throw expressions in the default constructor and copy constructor of Stack are benign, by and large. When either of these constructors throws an exception, no Stack object remains and there is little left to say. (The little that does remain is sufficiently subtle that I will defer it to a later column as well.)

Stack의 기본 생성자와 복사 생성자가 던지는 예외는 대체로 별 문제가 없습니다. 이 두 생성자에서 예외가 발생하면 어차피 Stack 객체가 남아 있질 않게 되니 할 말이 거의 없습니다.(아주 조금 할 얘기가 있습니다만 이것도 다음 칼럼에서 얘기하도록 하겠습니다.)

The throw from push is more interesting. Clearly, a Stack object that throws from a push operation has rejected the pushed value. However, when rejecting the operation, in what state should push leave its object? On push failure, this stack class takes its object into an inconsistent state, because the increment of top precedes a check to see that any necessary growth can be accomplished. The stack object is in an inconsistent state because the value of top indicates the presence of an element for which there is no corresponding entry in the allocated array.

push 연산이 던지는 예외는 좀 더 흥미롭습니다. push 연산 실행 도중 예외가 발생하면 Stack 객체는 push된 값을 저장하지 않습니다. 그럴 경우 push 완료 후Stack 객체가 어떤 상태가 될까요? push가 실패하면 Stack 객체 상태가 불일치하게 됩니다. 왜냐하면 메모리 공간을 확실히 확보했는지 확인하기 전에 미리 top을 증가시켜 버리기 때문입니다. 예외가 발생하면 top 변수값이 실제로는 있지도 않은 객체를 가리키게 되므로 서로 불일치하게 됩니다.

Of course, the stack class might be documented to indicate that a throw from its push leaves the object in a state in which further member functions (count, push and pop) can no longer be used. However, it is simpler to correct the code. The push member function could be modified so that if an exception is thrown, the object is left in the state that it occupied before the push was attempted. Exceptions do not provide a rationale for an object to enter an inconsistent state, thus requiring clients to know which member functions may be called.

물론 Stack 클래스 문서에 push 도중 예외가 발생하면 더 이상 다른 멤버 함수들(count, push 및 pop 등)을 사용할 수 없다고 기술할 수도 있겠지만, 그러느니 코드를 고치는 게 훨씬 간단할 것입니다. push 안에서 예외가 발생하더라도 객체 상태를 push 호출 전 상태에 머무르도록 push 멤버 함수를 수정할 수 있을 것입니다. 예외가 발생했다고 하여 불일치 상태가 되어서는 안될 것입니다. 그렇게 하면, Stack 사용자가 예외 발생 후에 일일이 어떤 멤버 함수를 호출할 수 있는지를알아야 하니까요.

A similar problem arises in operator=, which disposes of the original array before successfully allocating a new one. If x and y are Stack objects and x=y throws the out-of-memory exception from x.operator=, the state of x is inconsistent. The value returned by x.count() does not reflect the number of elements that can be popped off the stack because the array of stacked elements no longer exists.

operator=에도 비슷한 문제가 있습니다. 즉, 새로운 배열을 성공적으로 할당하기 전에 원래 배열을 반납해 버리고 있습니다. x와 y가 Stack 객체이고, x=y 수행 도중 메모리가 고갈되었다는 예외가 발생했다면 x의 상태는 불일치하게 됩니다. 스택에 저장된 객체 배열이 더 이상 존재하지 않으므로 x.count() 가 돌려주는 값은 실제 스택에서 꺼낼 수 있는 객체 개수를 정확히 반영하지 못하게 됩니다.

Exceptions Thrown by T / T가 던지는 예외

The member functions of Stack create and copy arbitrary T objects. If T is a built-in type, such as int or double, then operations that copy T objects do not throw exceptions. However, if T is another class type there is no such guarantee. The default constructor, copy constructor and assignment operator of T may throw exceptions just as the corresponding members of Stack do. Even if our program contains no other classes, client code might instantiate Stack<Stack<int> >. We must therefore analyze the effect of an operation on a T object that throws an exception when called from a member function of Stack.

Stack 멤버 함수들은 T 객체를 생성하기도 하고 복사하기도 하고 있습니다. T 가 int나 double과 같은 내장 형식이라면 T 객체를 복사하는 동안 예외가 발생할 리가 없습니다. 그렇지만 T가 클래스 형식이라면 얘기가 달라집니다. T의 기본 생성자, 복사 생성자, 할당 연산자 모두가 Stack과 마찬가지로 예외를 발생시킬 수 있습니다. 우리 프로그램안에 다른 클래스를 정의하지 않았다 하더라도 Stack을 사용하는 코드가 적어도 Stack<Stack<int> >를 생성할 수도 있는 노릇입니다. 그러니 Stack 멤버 함수에서 활용하는 T 객체 연산이 예외를 발생시킬 때, 어떤 일이 벌어질지 분석해 보아야만 하겠지요.

The behavior of Stack should be "exception neutral" with respect to T. The Stack class must let exceptions propagate correctly through its member functions without causing a failure of Stack. This is much easier said than done.

Stack은 T 객체에 대해 "예외 중립적(exception-neutral)"이어야 합니다. 즉, Stack 클래스는 T 객체에서 발생한 예외가 자신을 망가뜨리지 않으면서 멤버함수들을 통과하도록 해야 한다는 것입니다. 이게 정말 말처럼 쉬운 것은 아닙니다. 

Consider an exception thrown by the assignment operation in the for loop of the copy constructor:

복사 생성자에 있는 for 루프에서 할당 연산이 예외를 던지를 경우를 살펴 보겠습니다:

template <class T>
Stack::Stack(const Stack& s)
{
  v = new T[nelems = s.nelems]; // leak
  if( v == 0 )
    throw "out of memory";
  if( s.top > -1 ){
    for(top = 0; top <= s.top; top++)
      v[top] = s.v[top]; // throw 
    top--;
  }
}

Since the copy constructor does not catch it, the exception propagates to the context in which the Stack object is being created. Because the exception came from a constructor, the creating context assumes that no object has been constructed. The destructor for Stack does not execute. Therefore, no attempt is made to delete the array of T objects allocated by the copy constructor. This array has leaked. The memory can never be recovered. Perhaps some programs can tolerate limited memory leaks. Many others cannot. A long-lived system, one that catches and successfully recovers from this exception, may eventually be throttled by the memory leaked in the copy constructor.

복사 생성자가 예외를 잡지 않고 있으므로 Stack 객체를 생성하려고 했던 문맥까지 예외가 전파됩니다. 예외가 생성자 내부에서 시작됐으므로 객체가 아예 생성되지 않은 것이므로 소멸자도 호출되지 않게 됩니다. 그러니 복사 생성자가 할당했던 T 객체 배열을 삭제하지 않게 되고, 결국 해당 배열은 줄줄 세게 됩니다. 해당 메모리를 회복할 수 없습니다. 몇 몇 프로그램은 어느 정도 메모리 누수를 견딜 수 있겠지만 많은 프로그램들이 그럴 수 없습니다. 오랫 동안 실행되어야 하는 시스템은 이런 예외를 잡아서 정상 상태를 회복하면서 계속 실행하려고 노력할 것이고, 복사 생성자에서 발생한 메모리 누수로 인해 결국 메모리 부족으로 허덕이게 될 것입니다.

A second memory leak can be found in push. An exception thrown from the assignment of T in the for loop in push propagates out of the function, thereby leaking the newly allocated array, to which only new_buffer. points:

push에서도 메모리 누수가 발생합니다. push 안에 for 루프에서 T 할당시 예외가 발생하면 함수를 빠져 나가면서 new_buffer 가 가리키는 새 배열을 놓치게 됩니다:

template <class T>
void Stack::push(T element)
{
  top++;
  if( top == nelems-1 ){
    T* new_buffer = new T[nelems+=10]; // leak
    if( new_buffer == 0 )
      throw "out of memory";
    for(int i = 0; i < top; i++)
      new_buffer[i] = v[i]; // throw
    delete [] v;
    v = new_buffer;
  }
  v[top] = element;
}

The next operation on T we examine is the copy construction of the T object returned from pop:

다음으로 pop 에서 T 객체를 되돌려 줄 때 호출되는 T 객체 복사 생성자를 살펴 보겠습니다:

template <class T>
T Stack::pop()
{
  if( top < 0 )
    throw "pop on empty stack";
  return v[top--]; // throw
}

What happens if the copy construction of this object throws an exception? The pop operation fails because the object at the top of the stack cannot be copied (not because the stack is empty). Clearly, the caller does not receive a T object. But what should happen to the state of the stack object on which a pop operation fails in this way? A simple policy would be that if an operation on a stack throws an exception, the state of the stack is unchanged. A caller that removes the exception's cause can then repeat the pop operation, perhaps successfully.

T 객체 복사 생성자가 예외를 던지면 무슨 일이 벌어질까요? 스택 꼭대기에 있는 객체가 복사될 수 없으니 pop 연산이 실패할 것입니다. 호출자는 당연히 T 객체를 못 받겠지요. pop 연산이 실패한 스택 객체의 상태는 어떻게 되어야 할까요? 스택 객체에 어떤 연산을 수행하더라도 예외가 발생한 경우, 스택 상태가 바뀌지 않아야 한다는 단순한 정책을 따를 수 있을 것입니다. 이렇게 정해 놓으면 호출자는 예외 원인을 제거하고 다시 pop 연산을 다시 호출할 수 있을 것입니다.

However, pop does change the state of the stack when the copy construction of its result fails. The post-decrement of top appears in an argument expression to the copy constructor for T. Argument expressions are fully evaluated before their function is called. So top is decremented before the copy construction. It is therefore impossible for a caller to recover from this exception and repeat the pop operation to retrieve that element off the stack.

그렇지만, Stack의 pop은 되돌려 줄 값을 복사 생성할 때 실패하더라도 스택 상태를 바꿔 놓아 버립니다. v[top--] 수식이 T 복사 생성자의 인자로 들어갈텐데 이 수식이 완전히 평가된 후에 복사 생성자가 호출되므로 top 값이 감소된 후에 복사 생성자가 호출됩니다. 그러므로 호출자가 복사 생성자에서 발생한 예외로부터 회복하여 pop 연산을 반복하더라도 스택에서 마지막에 꺼내려던 그 객체를 꺼낼 수는없습니다.

Finally, consider an exception thrown by the default constructor for T during the creation of the dynamic array of T in operator=:

마지막으로 operator=에서 T 배열을 생성할 때 호출되는 기본 생성자가 예외를 발생시키는 경우를 살펴 보겠습니다:

template <class T>
Stack&
Stack::operator=(const Stack& s)
{
  delete [] v;  // v undefined
  v = new T[nelems=s.nelems]; // throw
  if( v == 0 )
    throw "out of memory";
  if( s.top > -1 ){
    for(top = 0; top <= s.top; top++)
      v[top] = s.v[top];
    top--;
  }
  return *this;
}

The delete expression in operator= deletes the old array for the object on the left-hand side of the assignment. The delete operator leaves the value of v undefined. Most implementations leave v dangling unchanged, still pointing to the old array that has been returned to the heap. Suppose the exception from T::T() is thrown from within this assignment:

operator=에서 delete 수식은 할당식 좌변 객체가 사용하는 배열을 삭제해 버립니다. delete 연산자를 수행하고 나면 v 값은 어떻게 될지 알 수 없습니다. 대부분 컴파일러들은 v 가 이미 힙으로 반납된 이전 배열을 가리키도록 그대로 놓아 둡니다. 그럼 할당 연산 안에서 T::T()를 수행하는 동안 예외가 발생했다고 가정해 보겠습니다:

{
  Stack x, y;
  y = x;  // throw
} // double delete

As the exception propagates out of y.operator=, y.v is left pointing to the deallocated array. When the destructor for y executes at the end of the block, y.v still points to the deallocated array. The delete in the Stack destructor therefore has undefined results - it is illegal to delete the array twice.

y.operator= 수행 도중 예외가 발생하면 y.v 는 반납된 메모리를 가리킨 채로 남게 됩니다. 이제 블록을 벗어 나려는 순간 y 에 대해 소멸자가 호출될 것인데, y.v 는 반납된 메모리를 여전히 가리키고 있으므로 어떤 일이 벌어질지 알 수가 없습니다 - 배열을 두 번 삭제하는 건 불법이죠.

An Invitation / 초대

Regular readers of this column might now expect to see a presentation of my version of Stack. In this case, I have no code to offer, at least at present. Although I can see how to correct many of the faults in Reed's Stack, I am not confident that I can produce a exception-correct version. Quite simply, I don't think that I understand all the exception related interactions against which Stack must defend itself. Rather, I invite Reed (or anyone else) to publish an exception-correct version of Stack. This task involves more than just addressing the faults I have enumerated here, because I have chosen not to identify all the problems that I found in Stack. This omission is intended to encourage others to think exhaustively about the issues, and perhaps uncover situations that I have missed. If I did offer all of my analysis, while there is no guarantee of it completeness, it might discourage others from looking further. I don't know for sure how many bugs must be corrected in Stack to make it exception-correct.

이제 독자 여러분은 제가 개선된 Stack을 제시해 주길 바라실 것 같습니다. 그렇지만 적어도 지금은 제시할만한 코드가 없습니다. Reed의 Stack에서 여러 결함을 어떻게 수정해야 할지는 알겠지만 제가 예외를 제대로 처리하는 버전을 만들 수 있을지는 확신하기 어렵습니다. 솔직히 저도 Stack이 처리해야만 하는 예외와 관련된 모든 상호 작용들을 완벽하게 이해하고 있지는 못하기 때문입니다. 그래서 Reed에게 (혹은 누구든지) 예외를 제대로 처리하는 Stack 을 공개하도록 초청하는 바입니다. 제가 이 글에서 제시했던 결함들만 해결한다고 예외를 제대로 처리하는 Stack 이라고 할 수 없습니다. 왜냐하면 제가 발견했던 모든 문제를 설명하진 않았기 때문입니다. 다른 분들도 이 문제에 대해 더 철저히 생각하여 제가 놓쳤던 문제들을 밝혀 내길 바라는 마음에 일부러 생략하였습니다. 제가 분석했던 내용이 어차피 완벽하지도 않겠지만 그걸 모두 제시해 버린다면 다른 분들이 더 깊이 보려고 하지 않을 터이니까요. Stack이 예외를 제대로 처리하게 하려면 얼마나 많은 버그를 수정해야 할지 저로서도 잘 모르겠습니다.