Creating Qt Threads Through Composition Rather Than Inheritance

Legacy Approaches Versus Modern Techniques

Understanding Historical Context

In early object-oriented programming practices, software engineers commonly extended system functionality through inheritance. This approach was prevalent in Qt applications where developers would subclass QThread and override the run() function to implement thread behavior.

However, modern software architecture principles strongly favor composition over inheritance. Inheritance should only represent actual "is-a" relationships defined by the requirements, not be abused as a convenient mechanism for code reuse.

Why Inheritance-Based Threading Falls Short

When subclassing QThread, the only meaningful difference lies in the void run() implementation. The interface remains identical across all thread subclasses. This creates several problems:

  • The override of run() serves no genuine architectural purpoce
  • It couples application logic unnecessarily to the threading framework
  • QThread represents an operating system thread wrapper
  • It should serve as a collection of thread operations, not a base for customization

A more flexible approach involves designating the thread entry point dynamically rather than hardcoding it into run().

Improved Threading Model Using Signals and Slots

Core Concept

The solution involves treating QThread as a compositional member rather than a base class. This pattern employs Qt's signals and slots mechanism to define the thread entry point:

  1. Declare a slot function (such as execute()) within the worker class
  2. Include a QThread member object in the class
  3. Relocate the current object to the new thread using moveToThread()
  4. Connect the thread's started() signal to the execution slot

Implementation: Heap-Allocated Thread Model

This implementation creates the thread object on the heap, allowing explicit lifecycle management:

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H

#include <QObject>
#include <QThread>

class WorkerThread : public QObject
{
    Q_OBJECT
public:
    static WorkerThread* createInstance(QObject *parent = nullptr);
    void launch();
    void stop();

private:
    explicit WorkerThread(QObject *parent = nullptr);
    ~WorkerThread();

    QThread* m_workerThread;
    QThread* m_previousThread;

protected slots:
    void execute();

signals:
    void finished();
};

#endif // WORKERTHREAD_H

WorkerThread.h

#include "WorkerThread.h"
#include <QDebug>

WorkerThread::WorkerThread(QObject *parent) 
    : QObject(parent), m_workerThread(nullptr), m_previousThread(nullptr)
{
    m_workerThread = new QThread();
    m_previousThread = QThread::currentThread();

    moveToThread(m_workerThread);

    connect(m_workerThread, SIGNAL(started()), this, SLOT(execute()));
}

WorkerThread* WorkerThread::createInstance(QObject *parent)
{
    return new WorkerThread(parent);
}

void WorkerThread::execute()
{
    qDebug() << "WorkerThread::execute running on thread:" << QThread::currentThreadId();

    for (int counter = 0; counter < 10; counter++)
    {
        qDebug() << "WorkerThread::execute iteration:" << counter;
    }

    qDebug() << "WorkerThread::execute completed";

    m_workerThread->quit();
    moveToThread(m_previousThread);

    deleteLater();
}

void WorkerThread::launch()
{
    m_workerThread->start();
}

void WorkerThread::stop()
{
    m_workerThread->terminate();
}

WorkerThread::~WorkerThread()
{
    delete m_workerThread;
    qDebug() << "WorkerThread destrucotr executed on:" << QThread::currentThreadId();
}

WorkerThread.cpp

Implementation: Stack-Allocated Thread Model

This simpler variant uses a stack-allocated thread object for scenarios where the thread lifetime matches the containing object's lifetime:

#ifndef EASYTHREAD_H
#define EASYTHREAD_H

#include <QObject>
#include <QThread>

class EasyThread : public QObject
{
    Q_OBJECT
public:
    explicit EasyThread(QObject *parent = nullptr);
    void launch();
    void stop();
    ~EasyThread();

protected slots:
    void execute();

private:
    QThread m_thread;
};

#endif // EASYTHREAD_H

EasyThread.h

#include "EasyThread.h"
#include <QDebug>

EasyThread::EasyThread(QObject *parent) 
    : QObject(parent)
{
    moveToThread(&m_thread);
    connect(&m_thread, SIGNAL(started()), this, SLOT(execute()));
}

void EasyThread::execute()
{
    qDebug() << "EasyThread::execute running on thread:" << QThread::currentThreadId();

    for (int counter = 0; counter < 10; counter++)
    {
        qDebug() << "EasyThread::execute iteration:" << counter;
    }

    qDebug() << "EasyThread::execute completed";
}

void EasyThread::launch()
{
    m_thread.start();
}

void EasyThread::stop()
{
    m_thread.terminate();
}

EasyThread::~EasyThread()
{
    m_thread.wait();
}

EasyThread.cpp

Usage Demonstration

#include <QtCore/QCoreApplication>
#include "EasyThread.h"
#include "WorkerThread.h"

void demonstrateStackThread()
{
    EasyThread worker;
    worker.launch();
}

void demonstrateHeapThread()
{
    WorkerThread* worker = WorkerThread::createInstance();
    worker.launch();
}

int main(int argc, char *argv[])
{
    QCoreApplication application(argc, argv);

    // Uncomment to test either approach
    // demonstrateStackThread();
    demonstrateHeapThread();

    return application.exec();
}

main.cpp

Note: The heap-based implementation contains a subtle race condition. The QThread::quit() function posts a quit event to the thread's event queue rather than terminating immediately. If the worker thread completes before the parent destructor runs, everything functions correctly. However, if the parent destroys the worker first, concurrent access may occur. Proper synchronization mechanisms should be employed for production code.

Understanding deleteLater()

Internal Mechanism

The deleteLater() function posts a deferred deletion event to the object's event queue:

void QObject::deleteLater()
{
    QCoreApplication::postEvent(this, new QEvent(QEvent::DeferredDelete));
}

Execution Context

Key behaviors of deleteLater():

  • The deletion event is posted to the event queue of the thread where the object currently resides
  • The actual deletion occurs when the event loop processes the deferred delete event
  • Execution happens in the thread where deleteLater() was called
  • Typically returns to the calling thread for cleanup operations

This mechanism ensures thread-safe cleanup without requiring explicit synchronization in most scenarios.

Summary

Early Qt versions forced developers to subclass QThread for thread creation. Contemporary best practices advocate composition over this inheritance-based approach. Modern Qt applications should:

  • Utilize composition to embed QThread objects as members
  • Employ signals and slots to define flexible thread entry points
  • Leverage moveToThread() for thread affinity management
  • Consider deleteLater() for proper resource cleanup

This pattern provides greater flexibility, looser coupling, and improved maintainability compared to traditional inheritance-based threading models.

Tags: Qt QThread multithreading signals-slots composition-pattern

Posted on Tue, 11 Aug 2026 16:53:14 +0000 by fotofx