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
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 *)
. - The constructor of the implementation class initialises its data members:
m_iface
andm_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
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.
- The body of the destructor
Model::~Model()
is executed. - The non-static data members of
Model
are 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.