Skip to content

Emitting Signals during Construction or Destruction

In the beginning, the application shows the Main Screen. When the user clicks on the Open button, the application will show a second screen. The second screen creates a C++ model when opened and destroys this model when closed.

As zealous Qt developers, we use the pimpl idiom to hide private declarations from its clients. Hence, we move the write and read functions of Qt properties to the implementation class in the source file. When the value of the property changes, the write function emits a signal of the interface class.

When the signal emission occurs during the construction or destruction of the interface class, we end up in the land of undefined behaviour. At the point of the signal emission, the interface object does not exist yet or does not exist any more. A real-life application will most likely crash – right a way if we are lucky or much later if Old Murphy has his way.

Setting Things Up

We define a property in the header file Model.h of the interface class Model:

Q_PROPERTY(QString infoText READ infoText WRITE setInfoText 
           NOTIFY infoTextChanged)

The read and write functions just forward the calls to the implementation class Model::Impl.

QString Model::infoText() const
{
    return m_impl->infoText();
}

void Model::setInfoText(const QString &text)
{
    m_impl->setInfoText(text);
}

The implementation class provides the implementation of the read and write functions.

QString Model::Impl::infoText() const
{
    return m_infoText;
}

void Model::Impl::setInfoText(const QString &text)
{
    if (m_infoText != text) {
        qDebug() << __PRETTY_FUNCTION__;
        m_infoText = text;
        emit m_iface->infoTextChanged();
    }
}

As usual, the write function emits a signal, if the value of the property infoText changes. The unusual thing is that the implementation class emits a signal of the interface class. Note the use of m_iface when emitting the signal. The member variable m_iface contains a pointer back to the interface object.

Emitting Signals during Construction

The constructors of the interface class Model and of the implementation class Model::Impl look as follows.

Model::Model(QObject *parent) :
    QObject{parent},
    m_impl{new Impl{this}}
{
    qDebug() << __PRETTY_FUNCTION__;
}

Model::Impl::Impl(Model *parent) :
    QObject(parent),
    m_iface(parent),
    m_infoText{QStringLiteral("Waiting...")}
{
    qDebug() << __PRETTY_FUNCTION__;
    setInfoText("Constructor: Oooops!!!");
}

The example code contains debugging outputs so that we easily see in which order functions are called. The trace for the construction of the Model object looks as follows.

Model::Impl::Impl(Model *)
void Model::Impl::setInfoText(const QString &)   // Undefined!!!
Model::Model(QObject *)

The initialisation order of classes, its base classes and its data members is defined in §15.6.2/(13) [class.base.init] of the C++17 Standard. When the QML component SecondScreen calls the Model constructor, the following steps occur.

  1. The non-static data members of the interface class Model are initialised first. So, Model::m_impl is initialised, which results in the call of the constructor of the implementation class: Model::Impl::Impl(Model *).
  2. The constructor of the implementation class initialises its data members: m_iface and m_infoText. Then, it executes its body, which results in calling void Model::Impl::setInfoText(const QString &).
  3. Then, it is time to execute the body of the Model constructor: Model::Model(QObject *).

According to §6.8/(1.1-1.2) [basic.life], the lifetime of the Model object begins when its initialisation is complete. This is only true after step 3. When the function Model::Impl::setInfoText calls the signal infoTextChanged on the Model object m_iface in step 2, this object does not exist yet. The behaviour of emitting the signal is undefined.

Emitting Signals during Destruction

The destructors of the interface class Model and of the implementation class Model::Impl look as follows.

Model::~Model()
{
    qDebug() << __PRETTY_FUNCTION__;
}

Model::Impl::~Impl()
{
    qDebug() << __PRETTY_FUNCTION__;
    setInfoText("Destructor: Oooops!!!");
}

The Model object is destroyed in the reverse order of its construction as the C++17 Standard (see note after §15.6.2/(13)) mandates and the trace shows.

virtual Model::~Model()
virtual Model::Impl::~Impl()
void Model::Impl::setInfoText(const QString &)   // Undefined!!!

When SecondScreen calls the destructor of the Model object, the following steps occur.

  1. The body of the destructor Model::~Model() is executed.
  2. The non-static data members of Model are destroyed, which results in calling Model::Impl::~Impl().
  3. The body of Model::Impl::~Impl() is executed, which results in calling void Model::Impl::setInfoText(const QString &).
  4. Finally, the non-static data members of the implementation class are destroyed.

According to §6.8/(1.3-1.4) [basic.life], the lifetime of the Model object ends, when its destructor is called. This happens in step 1. When the function Model::Impl::setInfoText calls the signal infoTextChanged on the Model object m_iface in step 2, this object does not exist any more. Hence, the behaviour of emitting the signal is undefined.

How to Fix

A fix must satisfy the following rule: The constructor and the destructor of the implementation class must not call member functions of the interface class. Corollary: They must not emit signals of the interface class. The interface object does not exist yet or does not exist any more.

An easy fix is to call Model::setInfoText in the body of the constructor or in the body of the destructor of the interface class Model. The trace for the construction looks like this:

Model::Impl::Impl(Model *)
Model::Model(QObject *)
void Model::setInfoText(const QString &) 
void Model::Impl::setInfoText(const QString &) 

In step 3, the Model constructor calls its own member function Model::setInfoText, which is perfectly legal C++ code according to §15.7/(3) of the C++17 Standard. The implementation object Model::Impl has been fully constructed at this point. So, Model::setInfoText can call Model::Impl::setInfoText, which can call Model::infoTextChanged without any problems.

The trace for the destruction is reversed:

virtual Model::~Model()
void Model::setInfoText(const QString &) 
void Model::Impl::setInfoText(const QString &)
virtual Model::Impl::~Impl()

In step 3, the Model destructor calls its own member function Model::setInfoText. At this point, the Model::Impl still exists so that the call to Model::Impl::setInfoText by Model::setInfoText is perfectly OK.

Getting the Example Code

The code is available on Github. You can try out the fix by uncommenting

//#define PROBLEM_FIXED

at the beginning of the file Model.cpp.

Leave a Reply

Your email address will not be published. Required fields are marked *