(Not so much) Fun with QSharedPointer
Qt has a wonderful way of dealing with memory management. The core idea is simple. Most objects have a parent, and when the parent gets destroyed, it will first destroy all its children. Using this technique, you can often write your Qt applications with little to no concern for memory management. Often, you literally don’t have to have a single delete
in your entire application. That’s pretty sweet!
In addition, since Qt 4.5 QSharedPointer
was introduced, which is very similar in concept to boost::shared_ptr
(and thus std::tr1::shared_ptr
). I have long been a huge fan of the idea of smart pointers. They solve the need to worry about memory management for almost all usual cases. Unfortunately, when you combine these two concepts, sometimes you can go awry. I was surprised by this one, so I figured I’d share my findings :-).
First, we need some basic code to demonstrate the issue… Let’s suppose we have two trivial classes which inherit from QObject
and thus participate in the Qt style memory management system. They might look like this:
Parent.h
class Parent : public QObject {
Q_OBJECT
public:
Parent(QObject *parent = 0) : QObject(parent) {
std::cout << "Parent()" << std::endl;
}
virtual ~Parent() {
std::cout << "~Parent()" << std::endl;
}
};
Child.h
class Child : public QObject {
Q_OBJECT
public:
Child(QObject *parent = 0) : QObject(parent) {
std::cout << "Child()" << std::endl;
}
virtual ~Child() {
std::cout << "~Child()" << std::endl;
}
};
And finally, we have some code to make use of our wonderful code above:
void MyAwesomeFunction(Child *o) {
std::cout << "Using Object!" << std::endl;
}
int main() {
Parent *const parent = new Parent(0);
Child *const child = new Child(parent);
MyAwesomeFunction(child);
delete parent;
}
The output of this simple program should be as follows:
Parent()
Child()
Using Object!
~Parent()
~Child()
As you can see, despite only explicitly deleting the parent
, the child
object gets cleaned up as well! This is great, heck if we made parent
an automatic object, we wouldn’t even need to explicitly delete
it either. But what if MyAwesomeFunction
for whatever reason, took a QSharedPointer
instead of a raw pointer? You may just assume that you could write your code to be like this:
void MyAwesomeFunction(const QSharedPointer<Child> &o) {
std::cout << "Using Object!" << std::endl;
}
int main() {
Parent *const parent = new Parent(0);
QSharedPointer<Child> child(new Child(parent));
MyAwesomeFunction(child);
delete parent;
}
well, if you made that assumption like I did, you’d be wrong! At best, this code leads to output like this:
Parent()
Child()
Using Object!
~Parent()
~Child()
QObject: shared QObject was deleted directly. The program is malformed and may crash.
At worst, it will crash. The question is, why is this broken? It all boils down to reference counting and who is doing the counting. What we have here, is two separate reference counts on the same object. One maintained by the parent
object, one maintained by the QSharedPointer
, and they will certainly disagree on when to delete
the object. This leads to double free and use after free errors. No Bueno. But it gets more complex than that! Qt memory management tries its best to let you do manual memory management while still maintaining its own system. So if you delete
child before the parent is deleted, that’s just fine. The child’s destructor will deregister itself from its parent’s child list. So the following is well-formed:
void MyAwesomeFunction(Child *o) {
std::cout << "Using Object!" << std::endl;
}
int main() {
Parent *const parent = new Parent(0);
Child *const child = new Child(parent);
MyAwesomeFunction(child);
delete child;
delete parent;
}
But if the parent happens to be delete’d first, then all hell breaks loose:
void MyAwesomeFunction(Child *o) {
std::cout << "Using Object!" << std::endl;
}
int main() {
Parent *const parent = new Parent(0);
Child *const child = new Child(parent);
MyAwesomeFunction(child);
delete parent;
delete child;
}
Segmentation fault When we use QSharedPointer
, this is essentially what we are doing if the object has a parent! The scary thing is, if the parent outlives the QSharedPointer
on average, then the bug doesn’t show up (since then it will act like case #1). Fortunately, starting with Qt 4.8, if you attempt to write this type of broken code, you will see an assert like this:
QSharedPointer: pointer 0x2384d70 already has reference counting
Which at the very least gives us a basic idea that there is something wrong, and it involves a QSharedPointer
. It’s a start. The simplest approach to the problem is to simply not mix and match the two memory management schemes. If you need a QSharedPointer
, don’t set the parent. Like this:
void MyAwesomeFunction(const QSharedPointer<Child> &o) {
std::cout << "Using Object!" << std::endl;
}
int main() {
Parent *const parent = new Parent(0);
QSharedPointer<Child> child(new Child());
MyAwesomeFunction(child);
delete parent;
}
which will happily output the following:
Parent()
Child()
Using Object!
~Parent()
~Child()