In my last article I wrote a little about the initialization side of RAII, Resource Acquisition Is Initialization, in this one I will talk about the flip side, RAII is also about destruction and cleanup.
Over the years I have read many times how modern RAII is more about object destruction and cleanup than it is about acquisition and initialization.
Many paths to destruction, need to catch them all
There are many ways objects are destroyed. Local objects are created inside one scope, and are destroyed when the scope is exited. Dynamically allocated memory can be gracefully released causing destruction to occur. Exceptions can trigger stack unwinding, destroying objects as part of stack unwinding. Garbage collection systems can decide an object is no longer used and destroy it.
If your object has any significant resources inside it those need to be cleaned up. Your object may contain pointers to other objects. It may contain OS resources like mutex handles or video card graphics buffers. It may contain buffered data waiting to be written. If destruction immediately released the memory you would have a similar problem to the initialization problem. Instead of some or all of your data being corrupt or invalid at startup, some or all of your data would be lost at destruction.
Back in the bad old days, any time you wanted to clean up an object you would need to remember to look for the conditions around the object’s destruction and remove the elements yourself. Sometimes a function would be written so there was only a single piece of code to change when the behavior was modified, and everybody everywhere needed to remember to call the function.
Remembering to call functions is error prone, so newer languages, starting in the 1980s, introduced ways to automatically clean up. The cleanup functions, called destructors in many languages, allowed programmers the opportunity to release buffers, to flush data, and to otherwise do what needed to be done to clean up gracefully.
These destructor functions, once standardized, could be called automatically by any of the normal destruction means. If you leave scope by ending a function or ending a block, the functions could be called. If you destroy dynamic objects, the functions could be called. If an exception is called and the stack gets unwound, the language can require the functions get called and the objects get cleaned up.
What belongs in a destructor?
In the simplest terms, destructors need to release whatever they have allocated. If they allocated memory, they need to release it. If the object contains sub-objects, they need to be destroyed, which means their destructors are called, and any sub-objects they use are destroyed and their sub-object destructors are called, and any sub-sub-objects are destroyed with destructors called, etc.
Often developers will create functions with allocate and release pairs, called a source and a sink. Create a thing, and destroy a thing. Obtain a resource, and release a resource. Open a file, and close a file. These are great to leverage with a destructor. If you remember to build your objects with sources and sinks, your destructor can call all the functions that release resources one after another. When they’ve all been called your object should be all cleaned up.
Sometimes, however, cleaning up objects can be slow. When program have a lot of resources it can take time to clean them up. If you have many gigabytes of data allocated in thousands or millions of objects, as is common in games, it can take many minutes for cleanup to take place.
This leads to a secondary system which is common in games and a few other industries: Since destruction of many objects can be slow, build objects so they don’t need destructors at all.
When you are burning down the building, don’t take out the trash
Sometimes when building software, you have situations where huge amounts of data need to be discarded all at once. You generally know about these events early on during product design.
As some examples in games, when you are done with a level you don’t want to neatly remove every object one at a time. You might have a pool strings for a translation database, or a pool of textures loaded from disk for display, or a pool of text files used for game scripts. When those are no longer needed there is no special cleanup, you just need the memory freed. There is nothing to save, no cleanup that needs to happen, you are destroying all of it in one pass. You need to provide a mechanism to dump all the objects without cleanup.
In these situations, objects such as container classes and resource pools can include an additional function that allows destruction without cleanup. It requires some care during implementation to ensure none of the contained objects need special cleanup or system resources. When the time comes for the entire pool of objects to be released, your code can then call this extra function that does not call the standard destructor, but instead deallocates the entire memory pool.
Writing code using memory pools is beyond the scope of this article, but systems like EASTL’s memory pools and Boost’s memory pools have this functionality built in. You can either use these excellent tools, or learn from them so you can build your own custom tools.
Be careful with this, since it only works with objects that don’t rely on system resources.
In summary, object cleanup is as important as object creation. Knowing your object’s lifetimes is critical in programming. Make sure they get cleaned up properly.