"Pointers are simple creatures. They were just like ions: if you make them be in somewhere, they'll be in somewhere, and that's it.
Me
I think you'll be frustrated if you see the following messages:
It might be the result of some kind of weird runtime logic errors. Quoting Beginning C++23 by Mr. Ivor Horton and Mr. Peter Van Weert at Apress™ publishers verbatim (legal disclaimer: not for commercial use! Just for educational and tutorial purposes only),
Dangling pointer is a pointer variable that still contains the address to free store memory that has already been deallocated by either
delete
ordelete[]
. Dereferencing a dangling pointer makes you read from or, often worse, write to memory that might already be allocated to and used by other parts of your program, resulting in all kinds of unpredictable and unexpected results.
Now, I assume that you all guys have been tortured by dangling pointers and null pointers, right? The Standard gives some solutions, such as include the <memory>
and using the std::unique_ptr
and std::shared_ptr
, etc. But, (personally,) I do not really like the design: they are complicated in definitions, the errors like Implicit template instantiation
emerge everywhere, and I am not into class names with any underscores (this is just a style; but, man, we're doing C++, not Pascal, provided that even Python suggests you doing class names without underscores)
You may of course blame me for being to fastidious, I accept it by a hundred and twenty percent. But that's me. I am not satisfied. I must find a better alternative.
To master this topic, we must first know what is the C++ philosophy regarding resources allocations, or even better, how every mainstream compiler works under the hood. So, as mentioned by the creator of C++, Bjarne Stroustrup, C++ advocates allocating objects in the constructors of an object, and freeing objects in the destructors of such object. This is commonly known as RAII (Resource Acquisition Is Initialization). Given that there are no garbage collectors in C++ that would automatically release unused objects, we programmers must be very careful on checking tediously the memory leaks (i.e. in simple English, memory wastage). Will there be a better solution?
Fortunately, Qt (in many and at most time) just stood there as a life-savior. The munificent developers meticulously crafted classes such as QPointer
, QSharedPointer
, QWeakPointer
, etc. Let us dive into them now, in this article.
If you ask me the reason why I cover this topic, is that I just mentioned the pointers and memory managements of the Qt framework several times in the past articles. They're somewhat important, but they're not as crucial a topic to be covered as the dynamic properties (that the uncovered yet mentioned features when introducing the meta-object systems). However, as a continuation to learn the meta-object system, I eventually found out that you would prefer it if you learned the smart pointers and did a good practice on them.
QPointer
Close analogy: std::auto_ptr
A typical template class QPointer
is responsible for supervising the pointers of QObject
(and its derivatives, of course). You could use it as an absolutely ordinary C/C++ pointer using the operator ->
, and also you could access the pointer's meta information via the .
operator and the methods with self-explanatory names like QPointer<T>::clear
, QPointer<T>::data
, QPointer<T>::swap
and QPointer<T>::isNull
, etc.
Particularly the method QPointer<T>::isNull
checks that if the supervised QObject
instance is usable. Under that circumstance, the method will return true
. Otherwise, it will return false
and the QPointer
's data will automatically be 0
.
The magic happens when
QPointer
overrode operators like*
,/
,-
,>
,!=
, etc.
Practicality and usages
If you kept a primitive C++ pointer to an object of any type and when the pointer is deleted (that it would become a raw pointer), if you accessed it, a segmentation fault error would arise (as shown in the above picture). Instead, you found out that it is more convenient and practical to use the QPointer
class (or other Qt-made memory managements classes). Here's a small example:
// ...
QPointer<QLabel> label = new QLabel("Testing");
// ... (may be destructive operations to the label object)
if (label)
label->show();
In the above example, remember not to add an asterisk after the QLabel
lexeme (as in QPointer<QLabel*>
, which is wrong and produces a QLabl**
class).
Also, the label
is automatically converted to bool
, which is approximately similar to determining the accessibility of an ordinary and primitive C++ pointer, except that the former one uses Qt code-bases and use smarter methods (for example, the above code will be converted to if ((!label.isNull()) && (label != 0))
), while the latter one just check if the pointer is equal to 0x0
or not, which might not be effective in checking dangling pointers.
QSharedPointer
Close analogy: std::shared_ptr
There's an even more meticulous class called QSharedPointer<T>
.
It uses reference counting internally, i.e. (for beginners) if the object belonging to QSharedPointer<T>
was being deleted, provided that no other objects referenced it (i.e. its reference count was 0), then such object (which was pointed by the pointer) was deleted.
Warning: If you gave a pointer to
QSharedPointer<T>
, do not pass it as an argument directly as aQSharedPointer<T>
(e.g. given the codeObj*obj=new Obj; QSharedPointer<Obj> objPtr(obj); void func(QSharedPointer<Obj>&)
, do callfunc(objPtr)
instead offunc(obj)
). Also, do not delete the pointer elsewhere throughout your codebase. Do not.For the reason why, as you might have already figured out, is that this would cause the reference counting system to moan. Given that it could not handle deletion of primitive pointers using the primitive
delete
operator elsewhere, the reference counting system hence omitted the existence of such code. Then, accessing it might causeSIGSEGV
(as you all have been... well, I never rub salt into the wound).Instead, you could use the
=
operator to copy aQSharedPointer<T>
type to another, and they both shared the same pointer. This time, the internal reference counting will be correctly increased by 1, and Qt could assure that you're pointer-safe and never encounter anySIGSEGV
(as you... well, well).
Practicality and usages
If you do not want to care about when and where your object will be deleted (in the meantime you hope that it will not be deleted), and the accomplish the garbage collection feature, use QSharedPointer<T>
. Here's a simple example, and I just need a main.cpp
file with <QtCore>
only:
#include <QDebug>
#include <QSharedPointer>
class Debugger
{
public:
Debugger()
{
qDebug() << "Debugger constructor called.";
qDebug() << "";
}
~Debugger()
{
qDebug() << "Debugger destructor called.";
qDebug() << "";
}
public:
void debug(const QString &message)
{
qDebug() << "Debugger debugged called:";
qDebug() << " " << message;
}
};
class ImportantObject
{
public:
ImportantObject(QString name)
: m_name(name)
{}
~ImportantObject()
{
m_debugger->debug(QString("%1 exit").arg(m_name));
}
public:
void setDebugger(QSharedPointer<Debugger> &debugger)
{
m_debugger = debugger;
}
private:
QString m_name;
QSharedPointer<Debugger> m_debugger;
};
int main(int argc, char *argv[])
{
// Scopes:
// Think like functions, where the variables are not usable outside of it
// Sometimes called closures
ImportantObject *obj1 = new ImportantObject("Important Object I");
{
QSharedPointer<Debugger> debugger(new Debugger);
{
QSharedPointer<Debugger> copiedDebugger(debugger);
}
obj1->setDebugger(debugger);
{
ImportantObject obj2("Important Object II");
obj2.setDebugger(debugger);
}
}
delete obj1;
return 0;
}
Here's the result:
Debugger constructor called.
Debugger debugged called:
"Important Object II exit"
Debugger debugged called:
"Important Object I exit"
Debugger destructor called.
It is evident that the memory is automatically managed.
QWeakPointer
Close analogy: std::weak_ptr
Everyone does not want to be weak, except for memory management. A weak pointer is weak, because you could not access the pointer directly (only possible through QWeakPointer<T>::data
); it is not guaranteed to be safe to access the pointer (i.e. there are chances that the referenced pointer was already deleted).
QWeakPointer<T>
was largely identical to QPointer<T>
, except that it could not use solely, and it must be paired up with QSharedPointer<T>
. Also, though it could be converted to an equivalent QSharedPointer<T>
object through QWeakPointer<T>::toStrongRef
, it is too inconvenient that you still need to check if the upgraded QSharedPointer<T>
object is null or not.
If it is that troublesome, why some of us still opt to use it?
That's approximately equal to the relationship of std::string_view
and std::string
. In the former one, you could not assign, modify, twist and bend the string. You could only view it. However, some of us still opt to use it because it's faster, as the indexing and modification processes are largely omitted. There's a similar analogy between a tuple
and a list
in Python.
Practicality and usages
It's speedy and satisfactory enough if we only need an observer to see if the object is null or not, rather than doing something to it.
So, here's a modified example from the example above, incorporating QSharedPointer<T>
and QWeakPointer<T>
:
#include <QDebug>
#include <QSharedPointer>
class Debugger
{
public:
Debugger()
{
qDebug() << "Debugger constructor called.";
qDebug() << "";
}
~Debugger()
{
qDebug() << "Debugger destructor called.";
qDebug() << "";
}
public:
void debug(const QString &message)
{
qDebug() << "Debugger debugged called:";
qDebug() << " " << message;
qDebug() << "";
}
};
class ImportantObject
{
public:
ImportantObject(QString name)
: m_name(name)
{}
~ImportantObject()
{
m_debugger->debug(QString("%1 exit").arg(m_name));
}
public:
void setDebugger(QSharedPointer<Debugger> &debugger)
{
m_debugger = debugger;
}
private:
QString m_name;
QSharedPointer<Debugger> m_debugger;
};
int main(int argc, char *argv[])
{
// Scopes:
// Think like functions, where the variables are not usable outside of it
// Sometimes called closures
ImportantObject *obj1 = new ImportantObject("Important Object I");
{
QSharedPointer<Debugger> debugger(new Debugger);
QWeakPointer<Debugger> weakReference(debugger);
if (!weakReference.isNull())
qDebug() << "At I: Weak reference is not null\n";
{
QSharedPointer<Debugger> copiedDebugger(debugger);
weakReference = copiedDebugger;
}
obj1->setDebugger(debugger);
{
ImportantObject obj2("Important Object II");
obj2.setDebugger(debugger);
}
if (!weakReference.isNull())
qDebug() << "At II: Weak reference is not null\n";
}
delete obj1;
return 0;
}
Here's the result:
Debugger constructor called.
At I: Weak reference is not null
Debugger debugged called:
"Important Object II exit"
At II: Weak reference is not null
Debugger debugged called:
"Important Object I exit"
Debugger destructor called.
QScopedPointer
Close analogy: std::unique_ptr
QScopedPointer<T>
uses the operator new
to dynamically allocate objects on the stack, and it assures that at any time the dynamically created objects could be deleted successfully.
If you give the ownership of a certain object to QScopedPointer<T>
, retrieve it back is not possible. Also, it never transfers ownership to any other object or smart pointer.
Note that you could even assign the method of deletion during construction.
There are four built-in allocation helpers provided by QScopedPointer
, namely:
-
QScopedPointerDeleter
usesdelete
andnew
to allocate objects; -
QScopedPointerArrayDeleter
usesdelete[]
andnew[]
to allocate objects; -
QScopedPointerPodDeleter
(wherePod
means "Plain Old Data") uses C's (that's why it's plain and old)free
andmalloc
to allocate objects; and -
QScopedPointerDeleteLater
usesQObject::deleteLater
to delete objects wisely. Note that (presumably) it raises error on scoped pointers of non-QObject
-descendant.
They could be used like (from the official Qt manual):
QScopedPointer<int, QScopedPointerArrayDeleter<int> > arrayPointer(new int[42]);
QScopedPointer<int, QScopedPointerPodDeleter> podPointer(reinterpret_cast<int *>(malloc(42)));
I said built-in, because the mighty Qt framework even allows you to create your own allocation helpers:
struct ScopedPointerCustomDeleter
{
static inline void cleanup(MyCustomClass *pointer)
{
myCustomDeallocator(pointer);
}
};
QScopedPointer<MyCustomClass, ScopedPointerCustomDeleter> customPointer(new MyCustomClass);
... Where there must be a method static public inline void cleanup(TargetClass *pointer)
(Not Java though!)
Practicality and usage
If you had many control statements that were way too complex, you might want to use a QScopedPointer<T>
:
int function(Enum enum)
{
QScopedPointer<Class> myClass = new Class;
switch (enum)
{
case 1:
if (function1(myClass))
{
if (externalProcess(myClass))
return finalProcess(myClass);
else if (anotherProcess(myClass))
return 42;
else if (someFunction(myClass))
return -1;
errorProcess(myClass);
return 0;
}
else
{
return (yetAnother(myClass) ? 1 : 0);
}
break;
case 2:
externalProcess(myClass); break;
default:
return yetAnotherProcessOtherThanThose(myClass);
}
errorProcess(myClass);
return -42;
}
If you do not use QScopedPointer<T>
, it would be a mess.
QObject
QObject
is sometimes considered as also a half smart pointer. It's because that QObject
shows partial memory managements, but it hardly demonstrated as mighty management methods as of QScopedPointer<T>
or QSharedPointer<T>
, etc.
You have already seen many QObject
descendants having the following parameter at their constructors:
Constructor(QObject *parent = nullptr)
As an addendum to the "Hello, World!" example, let me now elucidate why setting the parent object transfers the ownership of itself to its parent, and also when the parent got destroyed, it also got destroyed.
Here's the complete destructor of The Base Class:
/*!
Destroys the object, deleting all its child objects.
All signals to and from the object are automatically disconnected, and
any pending posted events for the object are removed from the event
queue. However, it is often safer to use deleteLater() rather than
deleting a QObject subclass directly.
\warning All child objects are deleted. If any of these objects
are on the stack or global, sooner or later your program will
crash. We do not recommend holding pointers to child objects from
outside the parent. If you still do, the destroyed() signal gives
you an opportunity to detect when an object is destroyed.
\warning Deleting a QObject while it is handling an event
delivered to it can cause a crash. You must not delete the QObject
directly if it exists in a different thread than the one currently
executing. Use deleteLater() instead, which will cause the event
loop to delete the object after all pending events have been
delivered to it.
\sa deleteLater()
*/
QObject::~QObject()
{
Q_D(QObject);
d->wasDeleted = true;
d->blockSig = 0; // unblock signals so we always emit destroyed()
if (!d->bindingStorage.isValid()) {
// this might be the case after an incomplete thread-move
// remove this object from the pending list in that case
if (QThread *ownThread = thread()) {
auto *privThread = static_cast<QThreadPrivate *>(
QObjectPrivate::get(ownThread));
privThread->removeObjectWithPendingBindingStatusChange(this);
}
}
// If we reached this point, we need to clear the binding data
// as the corresponding properties are no longer useful
d->clearBindingStorage();
QtSharedPointer::ExternalRefCountData *sharedRefcount = d->sharedRefcount.loadRelaxed();
if (sharedRefcount) {
if (sharedRefcount->strongref.loadRelaxed() > 0) {
qWarning("QObject: shared QObject was deleted directly. The program is malformed and may crash.");
// but continue deleting, it's too late to stop anyway
}
// indicate to all QWeakPointers that this QObject has now been deleted
sharedRefcount->strongref.storeRelaxed(0);
if (!sharedRefcount->weakref.deref())
delete sharedRefcount;
}
if (!d->wasWidget && d->isSignalConnected(0)) {
emit destroyed(this);
}
if (!d->isDeletingChildren && d->declarativeData && QAbstractDeclarativeData::destroyed)
QAbstractDeclarativeData::destroyed(d->declarativeData, this);
QObjectPrivate::ConnectionData *cd = d->connections.loadAcquire();
if (cd) {
if (cd->currentSender) {
cd->currentSender->receiverDeleted();
cd->currentSender = nullptr;
}
QBasicMutex *signalSlotMutex = signalSlotLock(this);
QMutexLocker locker(signalSlotMutex);
// disconnect all receivers
int receiverCount = cd->signalVectorCount();
for (int signal = -1; signal < receiverCount; ++signal) {
QObjectPrivate::ConnectionList &connectionList = cd->connectionsForSignal(signal);
while (QObjectPrivate::Connection *c = connectionList.first.loadRelaxed()) {
Q_ASSERT(c->receiver.loadAcquire());
QBasicMutex *m = signalSlotLock(c->receiver.loadRelaxed());
bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m);
if (c == connectionList.first.loadAcquire() && c->receiver.loadAcquire()) {
cd->removeConnection(c);
Q_ASSERT(connectionList.first.loadRelaxed() != c);
}
if (needToUnlock)
m->unlock();
}
}
/* Disconnect all senders:
*/
while (QObjectPrivate::Connection *node = cd->senders) {
Q_ASSERT(node->receiver.loadAcquire());
QObject *sender = node->sender;
// Send disconnectNotify before removing the connection from sender's connection list.
// This ensures any eventual destructor of sender will block on getting receiver's lock
// and not finish until we release it.
sender->disconnectNotify(QMetaObjectPrivate::signal(sender->metaObject(), node->signal_index));
QBasicMutex *m = signalSlotLock(sender);
bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m);
//the node has maybe been removed while the mutex was unlocked in relock?
if (node != cd->senders) {
// We hold the wrong mutex
Q_ASSERT(needToUnlock);
m->unlock();
continue;
}
QObjectPrivate::ConnectionData *senderData = sender->d_func()->connections.loadRelaxed();
Q_ASSERT(senderData);
QtPrivate::QSlotObjectBase *slotObj = nullptr;
if (node->isSlotObject) {
slotObj = node->slotObj;
node->isSlotObject = false;
}
senderData->removeConnection(node);
/*
When we unlock, another thread has the chance to delete/modify sender data.
Thus we need to call cleanOrphanedConnections before unlocking. We use the
variant of the function which assumes that the lock is already held to avoid
a deadlock.
We need to hold m, the sender lock. Considering that we might execute arbitrary user
code, we should already release the signalSlotMutex here – unless they are the same.
*/
const bool locksAreTheSame = signalSlotMutex == m;
if (!locksAreTheSame)
locker.unlock();
senderData->cleanOrphanedConnections(
sender,
QObjectPrivate::ConnectionData::AlreadyLockedAndTemporarilyReleasingLock
);
if (needToUnlock)
m->unlock();
if (locksAreTheSame) // otherwise already unlocked
locker.unlock();
if (slotObj)
slotObj->destroyIfLastRef();
locker.relock();
}
// invalidate all connections on the object and make sure
// activate() will skip them
cd->currentConnectionId.storeRelaxed(0);
}
if (cd && !cd->ref.deref())
delete cd;
d->connections.storeRelaxed(nullptr);
if (!d->children.isEmpty())
d->deleteChildren();
if (Q_UNLIKELY(qtHookData[QHooks::RemoveQObject]))
reinterpret_cast<QHooks::RemoveQObjectCallback>(qtHookData[QHooks::RemoveQObject])(this);
Q_TRACE(QObject_dtor, this);
if (d->parent) // remove it from parent object
d->setParent_helper(nullptr);
}
In simple English, there are five crucial steps:
- Dereferenciation of the
QWeakPointer<T>
weak references; - Emit the signal
QObject::destroyed
; - Disconnect all signal-slots connections established;
- Delete the children; and
- Remove itself from the parent object.
So, if we have a class that could automatically manages such processes, it will be much more convenient. The class should supervise as much QObject
as possible just like QPointer<T>
, and it could demonstrate automatic memory managements just like QScopedPointer<T>
.
Introducing: QObjectCleanupHandler
.
You could use QObjectCleanupHandler::add
to add object, isEmpty
to determine if there is still living QObject
's, and you could also clear all objects using QObjectCleanupHandler::clear
.
Practicality and usage
I would normally make this class a resource manager, and I suggest you doing so as well. If you have multiple QObject
's that are roughly identical, you use QObjectCleanupHandler
. Personally, I think that this one is useful.
Here's a minimalistic example:
void SomethingsManager::run()
{
Manager *m1 = new Manager(this);
// ...
// ...
QObjectCleanupHandler cleanup;
cleanup.add(m1);
cleanup.add(m2);
// ...
exec();
}
Do take a break and refer to some C++ docs in order to grasp the concept of std::unique_ptr
and std::shared_ptr
, etc.
Top comments (0)