다음을 통해 공유


C++ RAII compared with Java Dispose pattern

Earlier today I wrote that: “The C++ destructor model is exactly the same as the Dispose and using patterns, except that it is far easier to use and a direct language feature and correct by default, instead of a coding pattern that is off by default and causing correctness or performance problems when it is forgotten.“ Niclas Lindgren agreed with the post but added:

...although you use the word coding pattern to confuse something simple with something that sounds trickier.

I was referring to the Dispose coding pattern. For the benefit of those who aren't familiar with the Java Dispose pattern, let me give two examples: first a simple one with just one resource, then a more complex one with three resources. The examples use .NET Framework types.

Consider this C++/CLI code:

String^ ReadFirstLineFromFile( String^ path ) {
StreamReader r(path);
return r.ReadLine();
}

The minimal C# equivalent is the “using“ patterns which semiautomates the Java Dispose pattern (I'm showing the {} around the block to be explicit that there's a block, although since there's only one statement you don't really need them):

String ReadFirstLineFromFile( String path ) {
  using ( StreamReader r = new StreamReader(path) ) {
    return r.ReadLine();
  }
}

The minimal Java equivalent is the Dispose pattern (this is what C#'s “using“ generates under the covers):

String ReadFirstLineFromFile( String path ) {
  StreamReader r = null;
  String s = null;
  try {
    r = new StreamReader(path);
    s = r.ReadLine();
  } finally {
    if ( r != null ) r.Dispose();
  }
  return s;
}

I called this Dispose pattern “a coding pattern that is off by default and causing correctness or performance problems when it is forgotten.“ That is all true. You have to remember to write it, and you have to write it correctly. In constrast, in C++ you just put stuff in scopes or delete it when you're done, whether said stuff has a nontrivial destructor or not.

But that example is still flattering to the Dispose pattern, because there's only one resource. Consider as a second example this C++ code that opens a message queue and echoes one message to two target queues (a main queue and a backup queue) -- and automatically correctly closes the queues:

void Transfer() {
MessageQueue source( "server\\sourceQueue" );
String^ qname = (String^)source.Receive().Body;
MessageQueue dest1( "server\\" + qname ), dest2( "backup\\" + qname );

  Message^ message = source.Receive();
dest1.Send( message );
dest2.Send( message );
}

The minimal correct Java equivalent is:

void Transfer() {
MessageQueue source = null, dest1 = null, dest2 = null;
try {
source = new MessageQueue( "server\\sourceQueue" );
String qname = (String)source.Receive().Body;
dest1 = new MessageQueue( "server\\" + qname );
dest2 = new MessageQueue( "backup\\" + qname );

    Message message = source.Receive();
dest1.Send( message );
dest2.Send( message );
}
finally {
if( dest2 != null ) { dest2.Dispose(); }
if( dest1 != null ) { dest1.Dispose(); }
if( source != null ) { source.Dispose(); }
}
}

In this case, the queues will be freed up eventually when the garbage collector runs (which it probably will). In the meantime, we've tied up scarce resources. Worse, we've tied up resources on other machines.

So the C++ destructor model is exactly the same as the Dispose and using patterns, except that it is far easier to use and a direct language feature and correct by default, instead of a coding pattern that is off by default and causing correctness or performance problems when it is forgotten.

Comments

  • Anonymous
    August 01, 2004
    The comment has been removed

  • Anonymous
    August 04, 2004
    Still a bit flattering for the dispose pattern. It's often good to wrap each Dispose() with another try that does nothing (or logs, sets a flag for big throw, etc...) on catch in case it throws an exception. This is the only way to guarentee that each Dispose() gets a chance to run.

    finally {
    if( dest2 != null ) { try { dest2.Dispose(); } catch(Throwable e) { } }
    if( dest1 != null ) { try { dest1.Dispose(); } } catch(Throwable e) { } }
    if( source != null ) { try { source.Dispose(); } } catch(Throwable e) { } }
    }

    finally {
    Exception badDisp = null;
    if( dest2 != null ) { try { dest2.Dispose(); } catch(Exception e) { badDisp = e; } }
    if( dest1 != null ) { try { dest1.Dispose(); } } catch(Exception e) { badDisp = e; } }
    if( source != null ) { try { source.Dispose(); } } catch(Exception e) { badDisp = e; } }
    if ( badDisp != null ) { ... }
    }

    A real world case is often with something like an appserver when using different third party utilities, where the appserver needs to stay up for a long time (or hopes to), and the utilites may or may not always do the right thing. For instance it would be awful to miss returning a connections to the pool or to run out of file descriptors on a unix system and bring the whole thing down.

    I think in real world common situations, this is the actual minimum, and still a huge difference. That is unless an exception in C++/CLI deterministic destruction would cause the others to not run as well.

  • Anonymous
    March 15, 2005
    <p>收集一些C++的资料(此Book Mark会持续更新):</p>
    <p>C++之父的术语表:<br />
    <a href="http://www.research.att.com/~bs/glossary.html" class="bb-url">Bjarne Stroustrup's C++ Glossary</a> url: <a href="http://www.research.att.com/~bs/glossar

  • Anonymous
    January 22, 2009
    PingBack from http://www.hilpers.fr/452705-gestion-memoire-et-new-cwnd/5

  • Anonymous
    January 22, 2009
    PingBack from http://www.hilpers.pl/218205-czy-thinking-in-c/7