Balanced functions in C++
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:
open
andclose
instd::fstream
lock
andunlock
instd::mutex
- the non-default non-copy constructor and
join
instd::thread
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.