Divide in C++ resource management

There seems to be a divide in ideas about C++ resource management, which I’m going to grossly exaggerate and call the “Stroustrup” and the “Sutter” positions, based on comments by these titans in recent online videos.  These are of course not opposing positions, merely shades of opinion that depend on the application domain and target environment.  But there does seem to be an underlying difference of emphasis on how resource management, and memory management in particular, are best dealt with in modern C++.

To summarise:

“Stroustrup” “Sutter”
Focus on stack object managing resource Focus on heap object conveniently managed by smart pointer
Prefer handle/body idiom Prefer smart-pointers (shared_ptr/unique_ptr)
Memory is just one of many resources (file handle, mutex, …) Memory is special because it’s the most common resource
While C++ is broadly used, C++ is the best language for embedded or other small-memory architectures and C++ training should stress techniques that,  while applicable anywhere, are most important in these environments C++ is one competing language in many application domains and C++ teaching should stress how productive developers can be with modern C++
Handle/body completely hides heap allocation as implementation detail Sharing heap objects is common, shared_ptr/unique_ptr make lifetime and sharing semantics clear
Sharing objects much more uncommon than people think, shared_ptr only useful in small number of cases Smart-pointers are a useful default choice that add safety with no serious overhead in most environments
Detailed understanding of object and resource lifetime is an important part of application development.  In small-memory architectures (including high-volume web-servers as well as small portable devices) this is key to keeping memory requirements down. Applications can be written as quickly in modern C++ with smart pointers as in other languages while still avoiding most memory issues common to C and early C++.  This may lead to higher memory usage than is optimal but with C++ this can be optimised later if needed.
ostream os( "out.txt" );
os << "Hello, world!";
// file closed automatically

 
string s( "Goodbye, cruel world!" );
// memory buffer managed by s cleaned up automatically

 
lock_guard<mutex> hold( my_mutex );
// safe code
// mutex released automatically
unique_ptr<Thing> thing( CreateThing() );
// use thing
// thing cleaned up automatically

 
shared_ptr<SubThing> subThing( thing->GetSubThing() );
// borrow subThing without worrying about lifetime of thing
// subThing cleaned up automatically

My own position is informed by both hearing and reading these opinions from people with much wider experience and more depth of knowledge than mine, but also strongly based on my own experience and every day practice, mostly in business application development.

The “Stroustroup” approach is more concerned with memory efficiency and detailed understanding of the application. The “Sutter” approach is more concerned with developer productivity which is more of a concern in environments where C++ has to compete with other high-level languages such as Java and C#. This makes the “Sutter” position very practical for most types of application development, even for some constrained environments such as smart-phones, but would be inappropriate for other environments such as pacemakers or critical real-time environments.

So my own position:

  • Each resource should be owned by one specific object
    • Safer under exceptions, easier to understand
    • You have two resources managed by your class? Each should be managed by a separate member variable in your class, your class should directly manage neither.
  • Handle classes preferred for other (non-memory) resources
    • Clear classes for lock, stream, socket, thread where copy/sharing semantics are very specific and need to be clearly considered.
  • shared_ptr/unique_ptr preferred for heap memory resources
    • In my application domain memory is by far the most common resource: 1000s of heap objects vs 10s of window handles, a few threads, a few file handles, a few network sockets
    • Factory functions for objects only exposing an interface/abstract-base-class are common and creating handle/body seems unnecessary when a smart-ptr gives same result
  • shared_ptr with custom delete function should mostly be avoided
    • Custom delete should only be used when it’s still just managing a heap object, not when the shared_ptr is actually meaning something quite different.
    • Custom delete functions are less readable than a custom handle object for non-memory resources – e.g. shared-ptr<FILE> file( fp, fclose );
    • Custom delete functions to run code-at-exit is clever but probably less readable than a custom object with your code in the destructor – e.g. shared_ptr<void> autoDoItLater( 0, doSomethingAtEndOfScope );
    • But a custom delete function is sometimes useful for safely using separate heaps or specific release functions from other libraries.  No semantic change, still just cleaning up heap memory, but modifying how the memory is cleaned up.
  • shared_ptr/unique_ptr sharing/copying semantics are clearer than handle/body semantics
    • A handle might be copyable but what does it mean?  Copying a stream would mean another open stream on the same file, changes from either are visible to both.  But copying a string gives a separate string, changes to one don’t change the other.  This can be documented but is not obvious from the interface.  This can be avoided by sharing the object with a pointer, maybe a shared_ptr – oh, back where we started.
    • Copying a stack object that might or might not be a handle (hidden implementation detail, remember)?  Copy a mutex and get another reference to the same OS mutex?  Or a new OS mutex with similar characteristics?  What if I didn’t know the mutex class was a handle to an OS object rather than a language feature, would that change how I’d guess?
    • Copy a shared_ptr and it’s clearly a shallow copy, and references the same shared heap object.  You could argue this is also just documentation but it’s in the name and it’s so common it quickly becomes internal knowledge.

 

Links:

 

4 Comments

  • Herb Sutter 01/08/2013

    I think you’re oversimplifying, and also taking my talk somewhat out of context. For example, my talk was more about the delta between C++98 and C++11 and about how this affects developers and closes the gap with other languages by removing things like dangling pointers if you use the smart pointer. That it largely closes that hole doesn’t mean you should use those “only” or “all the time” by any means.

    I agree with everything in the “Stroustrup” column and actually disagree with the last three cells of the so-called “Sutter” position that sounds like recommending overusing smart pointers — RAII objects are your friend and are the default in C++ for a good reason and this is a huge advantage of C++ that makes it simpler. Smart pointers are attractive compared to raw pointers, but best of all is when you don’t need to use pointers at all, which is even more of the time now with move semantics.

  • keith ray 01/08/2013

    I did some coaching of programmers writing C and C++ for embedded systems. The C programmers’ target environment didn’t have a heap, so C++ new/delete and associated smart pointers were out. But C was an improvement on assembly language for their productivity and unit testing.

    The C++ programmers, though they called their target environment “embedded,” really had lots of memory and full-fledged Linux or Linux-like operating systems. But my experience helping them has showed me that very few programmers know how to write code the “Stroustrup” way (and not that many writing the “Sutter” way as well.). Often their use of C++ was a hindrance to productivity, testability, and execution-efficiency.

    If I could go back in time to advise those C++ “embedded” teams, I’d recommend C instead of C++. It’s seems like it is harder to write untestable, inefficient code in C, compared to the nightmares I’ve seen in C++. I’ve noticed that C programmers seem more pragmatic than many C++ programmers. The C++ programmers often have rules in their heads about the “right” way to code, that they have to unlearn if they want to write testable, exception-safe, efficient code.

  • Carl 03/08/2013 Author

    @herb
    Of course, all views in this blog are my own. The views above are taken out of context to contrast the different ideas these videos evoked in my own mind.

    I did not mean to imply heap+shared_ptr preferred over stack, only that shared_ptr simpler than handle/body for domain objects that need to be on the heap. Thank you for pointing out this was unclear.

    @keith
    I have no experience of embedded systems or even “embedded” system, so know I have no understanding of the constraints. Last memory constrained environment I worked in had 640KB*.

    (* pre-Windows MS-DOS for the youngsters out there)

  • Chris Oldwood 17/08/2013

    After struggling with the implementation of non-value like object in C++ for umpteen years (http://chrisoldwood.blogspot.co.uk/2013/07/overdoing-references.html) I now find myself writing C# and suffering from the exact opposite! There things don’t behave value-like by default and the subject of shared resource ownership is entirely manual.

    I remember going through a phase were virtually every class I created was derived from boost:non_copyable by default because trying to work out what copying would mean was just too hard, especially when forcing management by shared_ptr was just so much easier!

    One of the legacies of backwards compatibility with C is almost certainly the direct use of them instead of a proper C++ façade. You can’t try to be a multi-paradigm language and then get upset that developers aren’t doing it “right”.

Leave a Reply

Your email address will not be published. Required fields are marked *