Affordances: Deterministic Destruction
This post is part of an ongoing series about Affordances and Programming Languages.
When working in an object-oriented programming language, it is important to consider object lifetime. That is, how does an object get created, and how does it get destroyed or cleaned up when we’re done with it? There’s also the related problem of releasing scarce resources such as file handles, socket connections, and locks/mutexes.
In an ideal world with unlimited resources, we wouldn’t have to worry about cleaning up after ourselves. We could just create objects and acquire resources and never worry about it again. Unfortunately, we don’t live in that kind of world, and so we need to be concerned about cleaning up after ourselves.
In C, this is all done manually. We need to ensure that we pair
malloc()
and free()
, fopen()
and fclose()
, etc. This can get
tedious and error prone.
Many languages today have automatic garbage collection, first introduced by John McCarthy for Lisp. For many years, garbage collection was considered too slow for real-world use, and so was not widely adopted. Today, that has changed and almost every language has it.
Automatic garbage collection is nice, but only deals with memory allocation. What about other kinds of resources? Those still require the manual pairing of acquisition and release.
In languages with blocks, like Smalltalk and Ruby among others, we can implement helper methods that acquire the resource, invoke a block, and then release the resource. For example:
Another option is to use an ensure
or finally
construct to achieve
the same goal:
C++ does not have automatic garbage collection (yet), but does have something that very few languages have: deterministic destruction semantics. In garbage-collected languages, we don’t have control over when (or even if) an object will be finalized by the garbage collector. However, in C++ we know exactly when that will happen.
Determinstic destruction is an affordance that enables the poorly-named but very useful Resource Acquisition Is Initialization (RAII) idiom. With RAII, we define a class that acquires the resource in its constructor and releases it in its destructor. Thus, we can tie the lifetime of the resource to the lifetime of an object. Because we know exactly when the object will be destroyed, we can control when we will release the resource.
Modern C++ makes heavy use of RAII. The standard library’s smart
pointers (std::unique_ptr
and std::shared_ptr
), file streams, and
mutexes are prime examples of this.
At the end of f()
, p
is automatically released by its destructor,
freeing up any memory allocated. This happens when f()
exits
normally, but also if there is an early return or if an exception is
thrown.
Consistent use of RAII in C++ makes code exception safe and eliminates most instances of resource leakage.
In today’s code, manual memory management is almost an anachronism. However, we often forget that automatic garbage collection doesn’t deal with other kinds of resources, and so we end up using different patterns for other resources than we do for memory management. RAII allows us to use exactly the same patterns and idioms for all resources, including memory.