Disabling narrowing conversions in signal/slot connections
A small new feature that I have added to Qt 5.8 is the possibility of disabling narrowing conversions in the new-style QObject::connect statement. In this short blog post I would like to share with you why I thought this was useful and therefore implemented it.
The problem
Since Qt 5.0, the new-style, PMF-based (pointer to member function-based) QObject::connect will check at compile time if the signal’s signature is compatible with the slot’s one.
For instance, let’s consider these two QObject subclasses:
class Sender : public QObject { Q_OBJECT signals: void signalWithInt(int i); void signalWithDouble(double d); void signalWithQString(QString s); }; class Receiver : public QObject { Q_OBJECT public slots: void slotWithInt(int i); void slotWithDouble(double d); void slotWithQString(QString s); void slotWithQVariant(QVariant v); }; Sender *s = new Sender; Receiver *r = new Receiver;
This is what happens with various connect statements between them:
connect(s, &Sender::signalWithInt, r, &Receiver::slotWithInt); // works connect(s, &Sender::signalWithDouble, r, &Receiver::slotWithDouble); // works connect(s, &Sender::signalWithQString, r, &Receiver::slotWithQString); // works
So far, nothing surprising — a perfect match between the arguments will make the QObject::connect statement happy.
Let’s try some variations:
connect(s, &Sender::signalWithInt, r, &Receiver::slotWithQString); // does not compile connect(s, &Sender::signalWithQString, r, &Receiver::slotWithInt); // does not compile connect(s, &Sender::signalWithDouble, r, &Receiver::slotWithQString); // does not compile connect(s, &Sender::signalWithQString, r, &Receiver::slotWithDouble); // does not compile
Here things become more interesting: since there is no conversion between an int and a QString (and vice versa), this code rightfully fails to compile; the same happens with double.
In this case, the improvement that we get over the “old”-style QObject::connect is that this error is at compile-time instead of runtime. Indeed, the same connect statements rewritten using the macro-based QObject::connect will not work and generate warnings on the console. For instance:
connect(s, SIGNAL(signalWithInt(int)), r, SLOT(slotWithQString(QString))); // compiles, fails at runtime
The catch
The new QObject::connect has another little-known interesting feature: it allows the compiler to perform implicit conversions between the arguments of the signal and the slot.
For instance:
// using int -> double connect(s, &Sender::signalWithInt, r, &Receiver::slotWithDouble); // compiles and works as expected // using the implicit QVariant(QString) ctor connect(s, &Sender::signalWithQString, r, &Receiver::slotWithQVariant); // compiles and works as expected
This is just not possible at all using the old-style syntax; one would need workarounds such as a “trampoline slot” that does the conversion and emits another signal with the converted argument.
However, having implicit conversions also means that this statement succeeds:
// double -> int conversion connect(s, &Sender::signalWithDouble, r, &Receiver::slotWithInt); // compiles and "works"
This may be unexpected, as people usually assume this conversion will not work. After all, while converting an int to a double is always possible without loss of precision, converting a double to an int may lose precision or just not be possible (if the double is out of range).
Unfortunately, this conversion is fully allowed by C++. Consider it, if you prefer, one of the C remnants in the C++ language. That is, this code:
double d = 3.14; int i = d;
This is 100% legal C++ that compiles and works as expected (d is truncated; if it can’t fit into an int, the program has undefined behavior).
Only with C++11 one can forcibly disable narrowing conversions, by using the new uniform initialization syntax (so it’s effectively an opt-in feature). For instance:
double d = 3.14; int i{d};
Although many compilers just warn about this, this C++ code is ill-formed, because of the narrowing conversion from double to int.
Since we don’t like dealing with undefined behavior, I wondered if it could be possible to achieve the same for Qt. That is, could we disable implicit narrowing conversions in QObject::connect?
The solution
I went ahead and implemented the necessary modifications. Starting with Qt 5.8 one can define the QT_NO_NARROWING_CONVERSIONS_IN_CONNECT macro to disable the implicit conversions that narrow (if you are curious, I implemented it in this patch).
If you are using qmake, add this to your .pro file:
DEFINES += QT_NO_NARROWING_CONVERSIONS_IN_CONNECT # ... other DEFINES for your project ...
With this macro defined, QObject::connect statements that would narrow the arguments do not compile any longer:
// under QT_NO_NARROWING_CONVERSIONS_IN_CONNECT defined connect(s, &Sender::signalWithDouble, r, &Receiver::slotWithInt); // does not compile!
This functionality is opt-in, as making it the default (and opt-out) may break valid source code. In the Qt Project we try to never introduce gratuitous source-incompatible changes.
Conclusions
My personal recommendation would be to always define QT_NO_NARROWING_CONVERSIONS_IN_CONNECT in your projects. It will contribute to your projects’ “hygiene factor” by removing the possibilities of dangerous connect statements. For this reason, Qt itself uses it on the entirety of its build (see here).
Also, don’t forget that the Qt Project is open to contributions. Every time you think “it would be great if Qt could do this little thing for me…”, don’t be afraid, and contribute to Qt yourself!
About KDAB
KDAB is a consulting company offering a wide variety of expert services in Qt, C++ and 3D/OpenGL and providing training courses in:
KDAB believes that it is critical for our business to contribute to the Qt framework and C++ thinking, to keep pushing these technologies forward to ensure they remain competitive.
awesome, and I also think that this should be enabled by default.
Hi,
I agree, but making it opt-out instead of opt-in is a source incompatible change, and those are frowned upon in Qt; maybe we’ll make it opt-out in Qt 6. A discussion of which kind of source incompatible changes are acceptable within the same major version of Qt is going on here: https://codereview.qt-project.org/#/c/182311/ .
Cheers,