DEV Community

Cover image for [Qt] VIII. Smart Memory Managements
Misinahaiya
Misinahaiya

Posted on

[Qt] VIII. Smart Memory Managements

"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:

SIGSEGV

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 or delete[]. 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();
Enter fullscreen mode Exit fullscreen mode

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 a QSharedPointer<T> (e.g. given the code Obj*obj=new Obj; QSharedPointer<Obj> objPtr(obj); void func(QSharedPointer<Obj>&), do call func(objPtr) instead of func(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 cause SIGSEGV (as you all have been... well, I never rub salt into the wound).

Instead, you could use the = operator to copy a QSharedPointer<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 any SIGSEGV (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;
}
Enter fullscreen mode Exit fullscreen mode

Here's the result:

Debugger constructor called.

Debugger debugged called:
     "Important Object II exit"
Debugger debugged called:
     "Important Object I exit"
Debugger destructor called.
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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:

  1. QScopedPointerDeleter uses delete and new to allocate objects;
  2. QScopedPointerArrayDeleter uses delete[] and new[] to allocate objects;
  3. QScopedPointerPodDeleter (where Pod means "Plain Old Data") uses C's (that's why it's plain and old) free and malloc to allocate objects; and
  4. QScopedPointerDeleteLater uses QObject::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)));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

... 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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

In simple English, there are five crucial steps:

  1. Dereferenciation of the QWeakPointer<T> weak references;
  2. Emit the signal QObject::destroyed;
  3. Disconnect all signal-slots connections established;
  4. Delete the children; and
  5. 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();
}
Enter fullscreen mode Exit fullscreen mode

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)