Un-deprecate your Qt project
The last post from my colleague Marc Mutz about deprecating Q_FOREACH
caused quite an uproar amongst the Qt developers who follow this blog.
I personally feel that this was caused fundamentally by a perceived threat: there is a cost associated to porting away a codebase from a well-known construct (Q_FOREACH
) to a new and yet-undiscovered construct (C++11’s range based for
), especially when the advantages are not that clear. (With stronger advantages, maybe people would be more keen to move away).
A somehow opposite argument can be applied to Qt itself, however. There is a cost at keeping Q_FOREACH
around in Qt. For instance:
- it actually generates worse code than a range-based
for
, so it’s not suitable for usage in a general purpose library such as Qt (remember: in a library, all the codepaths are hot codepaths for some user); - we must maintain its code, make sure that it compiles without warnings, it gets tested and works on all the reference platforms (over 50 at the time of this writing);
- we must document its usage, have examples about it, and explain to our C++11 users (or users who come from Boost) what the differences are between
Q_FOREACH
,Boost.Foreach
and the range basedfor
.
On deprecated APIs in Qt
This is not going to be another post about Q_FOREACH
; this is a blog post about API deprecation in general. And, indeed: all of the above arguments apply to any API that is going through the deprecation process.
Any product needs to evolve if it wants to remain competitive. If the development bandwidth is finite, from time to time there is the need to drop some ballast. This does not necessarily mean dropping working functionality and leaving users in the cold. At least in Qt most of the time this actually means replacing working functionality with better working functionality.
This process has being going on in Qt since forever, but since Qt 5.0 we’ve started to formalize it in terms of documentation hints and macros in the source code. This was done with a precise purpose: we wanted Qt users to discover if they were using deprecated APIs, and if so, let them know what better alternatives were available.
And since the very release of Qt 5.0.0 we’ve officially deprecated hundreds of APIs: a quick grep in qtbase
alone reveals over 230 hits (mostly functions, but also entire classes).
Here’s some good news: many developers are probably using deprecated APIs right now, and are not even noticing. Those APIs are working as expected, compile flawlessly, pass all the tests, and so on. Again, this has a reason: even though some API is deprecated, the Qt source compatibility promise holds. Our contract with our users is that we will keep all of our released APIs fully working for the entire Qt 5 major release, including the ones that have been deprecated.
Here’s some really bad news: many developers are probably using deprecated APIs right now, and are not even noticing! Apart from the possibility of not seeing those APIs any more in Qt 6, the real issue here is: developers are not using APIs that could make their code go faster, be more secure, or more flexible — that is, the APIs that are available right now to replace the deprecated ones.
There’s no excuse for deliberately maintaining a sub-standard codebase, so let’s get to work…
The deprecation macros
The big question is: how can Qt users figure out that they’re using deprecated APIs in the first place? Luckily for them, Qt has a solution. Every time an API gets deprecated, we tag it in the source code with a few macros. Let’s take a real-world example, from qtbase/src/corelib/tools/qalgorithms.h
:
#if QT_DEPRECATED_SINCE(5, 2) template <typename RandomAccessIterator> QT_DEPRECATED_X("Use std::sort") inline void qSort(RandomAccessIterator start, RandomAccessIterator end) { ... } #endif
There are two macros used here. The first one is QT_DEPRECATED_SINCE(major, minor)
, which conditionally expands to true or to false, depending on whether we want to enable or disable APIs deprecated up to (and including) Qt version major.minor. The parameters used in this example mean that the qSort
function has been deprecated in Qt 5.2.
The second one is QT_DEPRECATED_X(text)
, which carries a text (there’s also a version without an argument, called QT_DEPRECATED
). This macro conditionally marks the declaration as a deprecated, using a compiler-specific way; in standard C++14 this would correspond to the [[deprecated("text")]]
attribute. The text
argument represents a porting hint for the developer in case she gets a warning from the compiler; the warning would therefore suggest to use std::sort
instead of qSort
.
As you may’ve guessed, those conditionally make all the difference. Under which conditions do these macros trigger and let us know that we are using deprecated APIs? As we noted before, all of this machinery is actually disabled by default: user code compiles with no warnings even when using such APIs. The two macros work as follows:
QT_DEPRECATED_SINCE
QT_DEPRECATED_SINCE
allows us to get compile-time errors if we use deprecated APIs.
QT_DEPRECATED_SINCE(major, minor)
compares major, minor
with the Qt version represented by the QT_DISABLE_DEPRECATED_BEFORE
macro. This macro uses a version encoded as 0xMMmmpp
(MM = major, mm = minor, pp = patch).
The actual comparison that happens is this:
// QT_VERSION_CHECK turns its arguments in the 0xMMmmpp form #define QT_DEPRECATED_SINCE(major, minor) (QT_VERSION_CHECK(major, minor, 0) > QT_DISABLE_DEPRECATED_BEFORE)
If the comparison fails, the entire #if
block guarded by a QT_DEPRECATED_SINCE
gets discarded by the preprocessor. This means we will not get a declaration for a given name (like qSort
, in our example), and therefore trying to use it in our code will trigger a compile error, just as wanted.
Unless we specify otherwise, QT_DISABLE_DEPRECATED_BEFORE
is set automatically to 0x050000
, i.e. Qt 5.0.0. We can easily raise the bar, for instance by adding this line into our .pro
file:
# disable all the deprecated APIs in Qt <= 5.8 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050800
The reason why this deprecation functionality is versioned has to do with future-proofing. We cannot possibly foresee which APIs will get deprecated in Qt. (Actually, we can, because discussions happen in the open on the Development mailing list and on Qt’s code review system. But that’s another story.)
If we turn any usage of deprecated APIs into a hard error, we may run into problems if users of our software upgrade their Qt installation to a higher version than the one we used to develop it: our software may fail to compile on the higher version of Qt because it may use an API deprecated in that higher version but not in the one we used. And that just makes our users not happy.
Recommendation: set QT_DISABLE_DEPRECATED_BEFORE
to the highest version of Qt that you developed and tested your software against.
Note that this comes with a cost: any usage of deprecated APIs will cause compile errors, which must therefore be fixed immediately. If we need the software to still compile while porting away from the deprecated API, keep reading…
QT_DEPRECATED_X
QT_DEPRECATED_X
is a weaker form of QT_DEPRECATED_SINCE
— it lets your code compile, but makes the compiler generate warnings if deprecated APIs are used. Again, this is disabled by default; in order to actually have the compiler emit warnings, this feature needs to be explicitly enabled by defining the QT_DEPRECATED_WARNINGS
macro:
DEFINES += QT_DEPRECATED_WARNINGS # warn on usage of deprecated APIs
Recommendation: always have the QT_DEPRECATED_WARNINGS
macro defined.
We can even combine the QT_DEPRECATED_X
and the QT_DEPRECATED_SINCE
macros, for maximum effect:
# warn on *any* usage of deprecated APIs, no matter in which Qt version they got marked as deprecated ... DEFINES += QT_DEPRECATED_WARNINGS # ... and just fail to compile if APIs deprecated in Qt <= 5.6 are used DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050600
That’s it!
Upstreaming this work
During QtCon 2016 we realized that many Qt users do not know about these macros and do not enable them. For this reason, apart from writing this blog post, I decided to invest a little time trying to make them more visible when using Qt tooling.
Since Creator 4.2 you will find these macros enabled by default when creating a new project (commit); the same will happen when using qmake -project
in Qt 5.9 (commit).
Could porting away from deprecated APIs be fully automated?
That is a very hard question to answer. At KDAB we have lots of experience with Clang-based refactoring and porting tools, and we invest considerable time to tune them for both opensource projects (such as clazy) and customer-related work.
In general, we can expect some help coming from tooling, especially when the porting involves some minimal and straightforward refactoring. In other cases, things may not be so simple (cf. the comments in Marc’s blog post about Q_FOREACH
), and tooling will help only in a percentage of cases.
Anyhow, stay tuned for more news around this!
Conclusions
Deprecating APIs is a natural thing in software development. As more brand new features get added to upcoming versions of Qt, some old feature may get marked as deprecated.
Luckily, we know that thanks to the Qt source compatibility promise our software will not break; and we can easily know which deprecated APIs we are using in our projects by enabling the relative warnings or errors.
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.
The wording of QT_DISABLE_DEPRECATED_BEFORE=0x050600 makes it sound like it should disable APIs deprecated in Qt < 5.6.0 (not Qt <= 5.6.0)
Hi,
yes, you’re absolutely right. The check is actually inclusive, so it’s 5.6 and before.
Hi! I’m building with cmake. I do not define QT_DEPRECATED_X in my CMakeLists.txt file; nevertheless, I get deprecation warnings like this:
/home/bhaller/Desktop/SLiM/QtSLiM/QtSLiMWindow.cpp:1957:39: warning: ‘void QTextEdit::setTabStopWidth(int)’ is deprecated [-Wdeprecated-declarations]
1957 | textEdit->setTabStopWidth(tabWidth);
| ^
In file included from /home/bhaller/Qt/5.14.2/gcc_64/include/QtWidgets/QTextEdit:1,
from /home/bhaller/Desktop/SLiM/QtSLiM/QtSLiMScriptTextEdit.h:23,
from /home/bhaller/Desktop/SLiMgui_CMAKE/SLiMgui_autogen/include/ui_QtSLiMWindow.h:32,
from /home/bhaller/Desktop/SLiM/QtSLiM/QtSLiMWindow.cpp:22:
/home/bhaller/Qt/5.14.2/gcc_64/include/QtWidgets/qtextedit.h:203:24: note: declared here
203 | QT_DEPRECATED void setTabStopWidth(int width);
| ^~~~~~~~~~~~~~~
I’m aware of the deprecation, but can’t fix it because I want end users of my software to be able to build it with Qt versions back to 5.9.x (whereas this deprecation occurred in 5.10). I don’t understand why I’m getting the warnings even though I don’t define QT_DEPRECATED_X; I don’t want my end users to see these warnings. The warning mentions -Wdeprecated-declarations, so perhaps I could define -Wno-deprecated-declarations, but then, depending on the compiler used, the user will instead get a warning about an unrecognized warning flag, since -Wno-deprecated-declarations is not supported by all compilers. Is there a way to just get rid of these deprecation warnings, perhaps with a #define of some kind inside my .cpp file above my usage of deprecated APIs?