
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.
- The non-static data members of the interface class
Modelare initialised first. So,Model::m_implis initialised, which results in the call of the constructor of the implementation class:Model::Impl::Impl(Model *). - The constructor of the implementation class initialises its data members:
m_ifaceandm_infoText. Then, it executes its body, which results in callingvoid Model::Impl::setInfoText(const QString &). - Then, it is time to execute the body of the
Modelconstructor: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.
- The body of the destructor
Model::~Model()is executed. - The non-static data members of
Modelare destroyed, which results in callingModel::Impl::~Impl(). - The body of
Model::Impl::~Impl()is executed, which results in callingvoid Model::Impl::setInfoText(const QString &). - 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.