[gclist] Destructor functions and GC

boehm.PARC@xerox.com boehm.PARC@xerox.com
Wed, 13 Mar 1996 11:19:14 PST


David Stoutamire writes:

"I don't understand the abstraction boundary argument in this example.
Whether or not the file handle is closed manually or finalization is
used, there is a clear relationship between the buffers and the file
handle, which together are implementing an abstraction (eg. stream).
The file handle has to know about the buffers in the sense that the
buffers have to be flushed before the file handle is closed, and the
buffers have to know about the file handle in the sense that they use
it when they fill up.  So it's no big loss of encapsulation to have the
finalization code, whatever it is, obey the same rules that explicit
closing code would have had to follow.

I suspect I'm missing your point, because I don't see why you say this
has anything to do with whether the file buffer knows about the
internal representation of file handles.  Doesn't the encapsulation
argument work the opposite way?  If the programmer has to reason about
the heap topology (ie. acyclicity) to write correct finalization code,
then you are _requiring_ the abstraction violation.  If there aren't
finalization ordering guarantees based on pointer structure, this
doesn't come up."

My model is that there are two independently implemented abstractions, roughly
as follows.  They both implement a "write" procedure and a "close" procedure
plus some others.

1) Output stream.  Might be implemented as a file handle.  Or it might actually
write to an in-memory string.

2) Buffered output stream.  You create one of these by giving it exclusive
access to an output stream.  The new write operation buffers and occasionally
calls the write operation on the underlying output stream.

I claim the implementation of the buffered output stream should not know
anything about the underlying stream, except that it has write and close
operations.  An output stream certainly shouldn't know about buffered output
streams, since it might be used directly without buffering.  There might not be
an associated buffer.

The close operation on buffered output streams might invoke the close operation
on the underlying stream.  The close operation on a stream should do nothing
about buffers.  Finalization should invoke the close operation, except when
that's a no-op, as for writing to an in-memory buffer.

You do have to violate encapsulation somewhat in order to avoid cycles in
finalization ordering.  If you fail to do that, you have a performance problem,
in that you use too much memory.  You have a leak.  But one could argue that
this is more generally true; you can't usually reason about space complexity or
any kind of resource usage without looking below the interfaces, or at least
the usual interface specifications.

To be honest, I don't like this example 100%, in that you may actually care
whether and when the buffer gets written, and not just that the program doesn't
crash due to writing to a closed file.  (I'm not implying any memory access
errors with the word "crash" here.)  Thus this may not be the world's best use
of finalization, though it's probably appropriate in some cases.  But hopefully
it does illustrate the problem.

Hans