Continuing on the topics I talked about
at this year’s aKademy conference.
Most of the things I want to write here are not new to people who are
immersed in C++ and follow the books/presentations by Alexandrescu,
Sutter, and others. But, I’ve found that in our Qt-sub-culture, it is
often not the case.
Qt is very good at hiding the ugly parts of C++, but at the same
time, it sometimes hides too much.
Memory safety
One of the first things you learn in Qt is about the object trees and
ownership where the parent obect conviniently destroys its children
on its destruction.
IMO, this is one of the nicest features of Qt. But, sometimes, it
tends to provide a sense of false security. As trivial example, imagine
the following:
QThread *thread = new QThreadDerivedClass(this);
thread->start();
It looks nice - we are creating a new object, we pass it a parent
(this), so we do not need to worry about its destruction ourselves. The
issue is that the parent is often a long-lived object like a
QCoreApplication or a main window. This tends to end up in the thread
object not being destroyed until the application has been
terminated.
Now imagine that the above object is an image cache, and you’ll have
a quite substantial memory leak.
The parent-child ownership is a silver bullet in a lot of cases, but
not all.
What does a pointer mean?
So, lets return to the topic at hand - API design. One of the biggest
problems when using a new library is what the following declaration
means:
SomeType * someMethod();
Namely, the question here is what does the ’SomeType *’ mean. It can
mean quite a few things:
- (static) singleton instance
- should be disposed by the user
- creator-owned, creator disposes of it
- an optional value? (for example, parsing a number from a string
could return a null if parsing failed)
- position in an array? (this one is rarely used nowadays, we have
iterators for that case)
The problem here is deducing who owns the returned object, and are we
guaranteed to get a non-null result at all. It can not be deduced
without reading the documentation, and it could and should be.
1. Singletons
Since a singleton should be always present, there is really no point
in making it a pointer at all. Implementing a proper singleton should be
as easy as:
SomeType & instance()
{
static SomeType s_instance;
return s_instance;
}
It is thread-safe (in C++11), its declaration clearly states that it
returns a non-null object, and an object with a long lifetime.
2. Factories
The next are the factory functions that return an instance of the
object whose owner should be the callee, and the callee is responsible
for its destruction:
std::unique_ptr<SomeType> createObject(...);
Even if the callee forgets to save the returned value, nothing will
be leaked.
3. Caches, ref-counted
singletons, etc.
When we have a function that returns something that can be destroyed
at any time, or that should be destroyed only after everyone stops using
it, we can simply write
std::shared_ptr<SomeType> getObject(...);
// or
std::weak_ptr<SomeType> getObject(...);
The first one tells the callee that the object will exist for as long
as he wants to use it, but without the guarantee that it will be
destroyed immediately after.
While the later says that it has been given an object that could go
away at any point in time.
4. Optional result values
The last use-case is the most problematic one. Not because it has not
been solved, but rather because the necessary class has not yet been
provided in C++.
When it becomes the part of the standard, it will look something like
this:
std::optional<SomeType> parseTheType(...);
For the time being, you could use boost::optional, pair<bool,
SomeType> and similar.
If you wanted to give the failure error as well, without resorting to
throwing exceptions all over the place, my advice is to go and watch Alexandrescu’s
talk on “Systematic error handling in C++”.
Exception safety
Now, after we saw what can be done to the API to make the user’s life
easier when it comes to memory management, just a very short
note about the exception safety.
There is a reason why std::stack does not have a method pop that
returns a value, but has the separated top and pop. This is, again, one
of the things that should be known to most c++ developers, yet sometimes
you can even find some c++ book authors that take jabs at the standard
committee for making it that way, and not going for the more convenient
API.
I suggest everyone to look at some writings about this - the issue
itself gives a nice overview of things to watch out for when designing
API which should behave well in the exception-enabled environment.