[Development] On removing functions from the API, but not the ABI

Marc Mutz marc.mutz at qt.io
Wed Jul 13 17:04:33 CEST 2022


Hi,

On the occasion of it's first birthday[1], let me introduce you to 
QT_REMOVED_SINCE, the "new" standard mechanism for non-deprecation API 
evolution.

== The problem

As you might have seen in Qt headers, we often amend function overload 
sets, e.g. adding defaulted arguments at the end, porting from int to 
qsizetype, or from a string-ish overload set to QAnyStringView. Due to 
binary compatibility, we cannot actually change the old functions[2], we 
need to keep the old function in and add overloads that hopefully won't 
cause ambiguities in existing calls. This is why QStringView was 
engineed to not cause ambiguities with QString overloads in the same 
set[3]: it allowed us to add QStringView overloads to existing QString 
functions without having to worry about breaking user code.

But QByteArrayView/QByteArray and QString/QAnyStringView don't overload 
properly (cause ambiguities), so just adding overloads and comments a la

    // ### Qt 7: remove
    // ### Qt 7: merge the above overloads

or even pre-programming for Qt 7 with

   #if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
      Qt 6 code
   #else
      Qt 7 code
   #endif

don't work anymore. We actually need to remove the QString/QByteArray 
overload (or at least make it Q_WEAK_OVERLOAD) for the 
QAnyStringView/QByteArrayView overload to not cause ambiguities.

So, if on hand, we can't remove existing function (overloads) because of 
binary compatibility, yet need to remove them to avoid ambiguity errors 
in existing users, what does that leave us with?

== The solution

The solution is to mentally separate ABI and API. For BC clients, all 
that matters is the ABI, while for SC clients, all that matters is the 
API. So, let's separate the two, and maintain them independently.

Enter QT_REMOVED_SINCE.

Suppose, you'd like to replace a function taking a QByteArray with one 
that takes a QByteArrayView. This would be SC, but not BC (if the 
function is exported). To dissociate API and ABI, we QT_REMOVE_SINCE the 
old version and just add the new:

   + #if QT_MEEPLIB_REMOVED_SINCE(6, 4)
      void setFoo(const QByteArray &foo);
   + #endif
   + void setFoo(QByteArrayView foo);

we can even add back the BA overload as a weak one:

   + Q_WEAK_OVERLOAD
   + void setFoo(const QByteArray &foo);

In the .cpp file, we just change the function:

   ± docs
   - void Meep::setFoo(const QByteArray &foo)
   + void Meep::setFoo(QByteArrayView foo);
   ± impl

maybe add the weak overload, too:

   + docs
   + Q_WEAK_OVERLOAD_IMPL
   + void Meep::setFoo(const QByteArray &foo)
   + impl

So now we've done everything to make _new_ users pick up the new 
function. The QT_CORE_REMOVED_SINCE removes the old function from the 
API (it's not visible to the compiler anymore), but still we have a 
loose end to tie up: the function is gone from the ABI, too. In order to 
get it back, we need to add an implementation to a special file, 
removed_api.cpp. If this file doesn't, yet, exists for the library you 
work on, add it by mimicking [4]. Once it exists, find the 
QT_<module>_REMOVED_SINCE(x, y) section in it that matches the version 
of your QT_<module>_REMOVED_SINCE(x, y) in the header, and add the 
following:

   #if QT_MEEPLIB_REMOVED_SINCE(6, 4)

   +#include "meep.h"
   +
   + void Meep::setFoo(const QByteArray &foo)
   +{
   +    setFoo<>(foo); // calls the Q_WEAK_OVERLOAD
   +}
   +
   // #include "qotherheader.h"
   // // implement removed functions from qotherheader.h
   // order sections alphabetically

   #endif // QT_MEEPLIB_REMOVED_SINCE(6, 4)

et voila: the function is back in the ABI, but not in the API, just what 
the doctor prescribed.

== PS: On qsizetype

If you look at existing uses, you sometimes will see an additional guard 
QT_POINTER_SIZE != 4. This is because on 32-bit platforms, 
std::is_same_v<qsizetype,int>, so we can't have

    setMeepCount(int); // QT_REMOVED_SINCE
    setMeepCount(qsizetype); // new

because they're literally the same function. So we remove the int one 
only when it'd be different from the qsizetype one:

   #if QT_MEEPLIB_REMOVED_SINCE(6, 4) && QT_POINTER_SIZE != 4
     void setMeepCount(int count);
   #endif
     void setMeepCount(qsizetype count);

What needs to go into removed_api.cpp is left as an exercise to the reader.

Thanks,
Marc

[1] AuthorDate of https://codereview.qt-project.org/c/qt/qtbase/+/359671 
and https://codereview.qt-project.org/c/qt/qtbase/+/362209
[2] unless they're inline and not inside an exported class (which is why 
we shouldn't export non-polymorphic classes wholesale: it restricts what 
we can do to inline functions)
[3] https://www.kdab.com/qstringview-diaries-masters-overloads/
[4] https://codereview.qt-project.org/c/qt/qtbase/+/421325


More information about the Development mailing list