shared_ptr_nonnull and the Zen of reducing assumptions

(This article assumes some familiarity with shared_ptrs in C++.)

Imagine the following line of code and comment are in the private area of the definition of a C++ class Foo:

// The current Quaffle, always valid
shared_ptr<Quaffle> currentQuaffle;

Can you spot any dangerous thinking here? If not, that’s okay, but hopefully this article will change that.

Within the implementation of Foo, because currentQuaffle is assumed to be “always valid,” code dereferences it and uses the Quaffle it points to without checking for validity, i.e.:

currentQuaffle->DoTheThing();

rather than

if (currentQuaffle) {
    currentQuaffle->DoTheThing();
}

The bug introduced by this assumption was caused when an empty currentQuaffle crashed trying to DoTheThing(). A value had never been set after currentQuaffle was silently created using the shared_ptr default constructor.

It’s easy to imagine other ways a bug could be introduced here. Some other object might pass an empty shared_ptr into an instance of Foo without realizing it, maybe across many layers of the call stack. Or a future developer might call reset() on currentQuaffle inside Foo’s implementation without knowing it’s meant to always be valid. In all these cases, currentQuaffle ends up breaking an unenforced law that it should always be valid.

What’s the solution? Ideally, we could simplify the ownership of currentQuaffle so that Foo has a plain Quaffle rather than use a pointer at all. But if this isn’t feasible, we can still let the type of currentQuaffle encode the rule about validity rather than hoping that developers obey it. Enter shared_ptr_nonnull, a class invented to solve this problem. It’s just like a shared_ptr, except:

  • It lacks functions that would make it empty, i.e. a default constructor and reset() with no arguments, and
  • It fails an assert whenever it’s made empty, like when it’s constructed from an empty shared_ptr. (“Fails an assert” means it traps to the debugger in debug builds. This could arguably be an even stronger failure, but I’ll leave that topic alone for now.)

This class catches bugs at compile time, most often when something tries to default-construct a shared_ptr_nonnull member variable, and at runtime, when someone makes it empty in other ways.

In a nutshell, we were assuming currentQuaffle was always valid, and shared_ptr_nonnull gives us a way to make sure that’s true. I’ve seen this concept come up again and again in software development, across programming, debugging, testing, planning and more. Two of the most important questions you can ask yourself are “What am I assuming right now?” and “How can I make sure it’s true?”

The answer to “How can I make sure it’s true?” might be to write another unit test, to step through with the debugger in a slightly different context, to try a different manual test case, to do some user validation, or a whole host of other options. In this case, the solution was to write a new class. But it took a bug to make us write that class. Preferably, we would have seen that innocuous little phrase “always valid” as an alarm bell going off before we ran into a bug. It takes a particular kind of thinking to see past our own assumptions and we should push ourselves to think in that way as much as possible.

I used the term Zen in the title of this article not because I want you to write code in the lotus position but because Zen meditation focuses on a heightened awareness of things so innate you might never otherwise notice them, like your thoughts and your breathing. If we can train ourselves to be aware of our own innate assumptions, we can write more enlightened code. Thanks for reading, and good luck!

Leave a comment