Let’s say you have the following unmanaged code:
-
#pragma unmanaged
-
-
class Stream { … }; // Conceptual stream class
-
-
class StreamWriter
-
{
-
public:
-
StreamWriter(Stream* pStream) : m_pStream(pStream) {}
-
~StreamWriter() { /* Use m_pStream in some way */ }
-
-
…
-
private:
-
Stream* m_pStream;
-
};
-
-
void f()
-
{
-
Stream stream;
-
StreamWriter streamWriter(&stream);
-
-
// Use streamWriter
-
-
// streamWriter is destroyed
-
// stream is destroyed
-
}
Note that StreamWriter’s destructor uses m_pStream (perhaps by flushing the stream). This means that the order of destruction is important — StreamWriter must be destroyed before its underlying Stream is.
Now let’s try to write and use some simple managed C++ wrappers for these classes:
-
#pragma managed
-
-
public __gc class ManagedStream
-
{
-
public:
-
ManagedStream() : m_pStream(new Stream) {}
-
-
// NOTE: This is a finalizer, not a determinstic destructor
-
~ManagedStream() { delete m_pStream; }
-
-
public private: // Make accessible by ManagedStreamWriter
-
Stream __nogc* m_pStream;
-
};
-
-
public __gc class ManagedStreamWriter
-
{
-
public:
-
ManagedStreamWriter(ManagedStream* pStream) :
-
m_pStreamWriter(new StreamWriter(pStream->m_pStream)) {}
-
-
// NOTE: This is a finalizer, not a determinstic destructor
-
~ManagedStreamWriter() { delete m_pStreamWriter; }
-
-
private:
-
StreamWriter __nogc* m_pStreamWriter;
-
};
-
-
void f()
-
{
-
ManagedStream stream = __gc new ManagedStream();
-
ManagedStreamWriter streamWriter =
-
__gc new ManagedStreamWriter(stream);
-
-
// Use streamWriter
-
-
// GC will clean up stream and streamWriter
-
}
See the problem?
As I (gratituously) hinted above, we have a problem due to nondeterminstic destruction. Since the GC does not define the order in which it will destroy managed objects, we cannot guarantee that the ManagedStreamWriter will be destroyed before the ManagedStream. If the ManagedStream is destroyed first, then its Stream will be deleted before the ManagedStreamWriter’s StreamWriter destructor is called. This means that StreamWriter will be using a deleted pointer — a sure recipe for disaster.
I can think of a few possible solutions to this problem:
- Have the managed classes implement
IDisposable, and require developers to use it to achieve determinstic destruction. The main downside to this approach is “what if developers forget?” - Recapture the interdependencies among unmanaged classes in the managed wrappers. For example, add a reference in
ManagedStreamWriterto theManagedStreamobject. This will force the GC to properly order their destruction, and is probably the right way to go.
October 19th, 2007 at 11:13 pm
Hi.
I don’t see this as a problem managed code, because this is typically, a scenario where you should implement IDisposable, and use Dispose() to deterministically clean up the object.
In the managed world, you cannot rely on the order in which objects are finalized, and so you must finalize them yourself deterministically, and that’s what IDispose is for. If you implement IDispose on the StreamWriter, you can do the cleanup when Dispose() is called deterministically (e.g., not by the GC by way of Finalize()).
I also don’t see a legitimate case where a stream or file would be allowed to remain open until the GC gets around to finalizing the object responsible for flushing or closing it. Something like that should always happen deterministically.
November 1st, 2007 at 2:21 pm
Tony,
If you are a library writer, you cannot rely on clients doing the “right thing” and calling Dispose() like they should. Therefore, in addition to exposing the IDisposable interface, you must also clean up unmanaged objects in your finalizers.