Balanced functions in C++

2021 March 22

A balanced function is one of a pair of functions such that every call to the first function must be balanced with exactly one call to the second function, and vice versa, much like balancing brackets in an expression. There is often some associated object to carry state between the calls. The canonical examples are malloc and free, with their intermediate state represented by a pointer. new and delete[1] go one step further and balance a call to malloc plus a call to a constructor with a call to free plus a call to a destructor.

There are many examples throughout the standard library:

Other common names seen in the wild include (start, stop) and (connect, disconnect). For this post, I will follow a naming convention of start and stop.

RAII

The best way to ensure balanced function calls in C++ is by using constructors and destructors. Constructors and destructors are themselves balanced functions that, much of the time, the language will balance for us. We can piggy-back on that automatic balancing to balance calls to other functions.

For objects without dynamic storage duration (i.e. with automatic, static, or thread storage duration), the language guarantees that every call to a constructor is balanced with a call to the corresponding destructor, even in the presence of exceptions. Transitively, an object that enjoys balanced constructor and destructor calls will correctly balance those calls for its base classes and non-static members.

Balancing constructors with destructors for objects with dynamic storage duration is left as an exercise for the programmer, but the standard containers and smart pointers correctly balance them for the objects they manage. By avoiding direct contact with dynamic storage duration, e.g. by never directly calling new or delete, we can ensure that all the constructors and destructors in our program are correctly balanced.

This technique using constructors and destructors to balance function calls is named Resource Acquisition Is Initialization (RAII)[2].

One start function call per constructor body

Where's the potential bug in this code?

Application::Application() {
a_.start();
b_.start();
}
Application::~Application() {
b_.stop();
a_.stop();
}

If b_->start() throws, then the Application constructor will exit abnormally, after a_ has started. Because the constructor exited abnormally, the Application destructor will never be called, and thus the balancing call to a_->stop() will never happen.

It may be that a_ is a member who is destroyed when the Application constructor exits, but not all destructors can be expected to call the balancing stop function. For example, std::mutex::~mutex does not.

Rearranging the calls to start cannot fix the bug: what if a_->start() throws instead?

If a destructor balances a start function called by a constructor, then the constructor must ensure it does not call any function that may throw an exception after it calls the start function. Generally, the easiest way to follow this rule is to call only one start function per constructor body, and to leave the call as the last statement in the body.

If we want to call multiple start functions from a single constructor, then they need to be called by constructors of members of that class. If one of those member constructors exits abnormally, the language guarantees that the balancing destructors for every member constructor that finished before it will be called.

One RAII type per pair of balanced functions

Does this mean we must write a separate RAII type for every pair of balanced functions in our API? Yes! But it doesn't have to be "separate". Before adding start and stop methods to a type, I first see if I can move that behavior to the type's constructor and destructor instead, making the type itself follow RAII.

Often that's not an option. Perhaps the object can be started and stopped multiple times, but it can only be constructed and destroyed once. Or perhaps the start and stop functions are free functions, not methods. My next preferred technique is to make my start function return a RAII object that calls the stop function in its destructor. The caller can manage the stop object's lifetime, thus choosing when its destructor is called, while letting the language guarantee that it is called exactly once. To help callers use the API correctly, the start function return type should be annotated [[nodiscard]] and if the stop function is a method, it should be private. The stop object should be moveable but not copyable; a copy would call the stop function more than once.

Finally, if that is not an option, then I follow the pattern of std::lock_guard and add a new RAII type whose sole purpose is to balance calls to start and stop functions using its constructor and destructor.

If you prefer the convenience of a single type, these last two techniques can be combined by having a public start method return a RAII type that calls a private start method in its constructor.

Every pair of balanced functions must have a corresponding RAII type that balances those functions using its constructor and destructor. If such a RAII type is missing, the API will be difficult to use correctly.

Footnotes

  1. While technically "operators", they mimic functions in this context. ↩︎

  2. As a fan of acronyms over initialisms, I pronounce it "ray". ↩︎