Deprecation

From Qt Wiki
Revision as of 20:10, 23 January 2023 by EdwardWelbourne (talk | contribs) (→‎Think twice before deprecating: Note the recommended practice of adding new API before deprecating old.)
Jump to navigation Jump to search

Sometimes we replace one Qt API with another, or (less often) conclude that an API was a mistake and client code should discontinue using it. In such a case we first deprecate the old API and, later, remove it. When we do this we need to make it easy for maintainers of client code to migrate away from it.

Our backwards-compatibility commitments preclude unconditionally removing a deprecated public API before the next major version. Even then, however, it is advantageous to enable client code to suppress such an API, once the client code has been adapted to no longer use it. This ensures that contributors to the client code don't re-add new uses of the deprecated API after its maintainers do such an update. In the case of client code that compiles Qt for itself, it can also reduce the size of the Qt binaries.

For private APIs, it can also be useful to deprecate as part of the transition from an old API to a new, when the API is used by modules other than the one that defines it (for all that we generally aim to avoid such cross-module use of private APIs). This can make it easier for downstream modules to find their uses of the old API, in order to port away from it.

There are two places in which a deprecation needs to be put into practice:

  • the documentation, if the API is public, using the \deprecated [version] message QDoc macro, and
  • the declaration and definition, using various macros described below.

There are two things client code can use to configure how deprecations affect it:

  • they can select which versions' deprecations to be warned about,
  • they can select which versions' deprecated API to entirely omit.

Macro names

This article uses the macros QT_DISABLE_DEPRECATED_UP_TO and QT_WARN_DEPRECATED_UP_TO.

Those macros are introduced in Qt 6.5. If you are using an older version of Qt, use QT_DISABLE_DEPRECATED_BEFORE and QT_DEPRECATED_WARNINGS_SINCE respectively.

The old macros can still be used in Qt 6.5 and later, but only if the new macros are not defined. The related gerrit changes are:

Think twice before deprecating

Deprecation forces maintainers of client code to make changes to their code. Obliging them to make major changes to their code should not be embarked on lightly. Qt's own code will usually also need changes to adapt to a deprecation.

Those who introduce deprecations are responsible for fixing any resulting breakage in Qt (and the Qt Creator folk would appreciate help with this, too), including all warnings, since we treat warnings as errors. Doing this may give you some insight into how disruptive your deprecation shall be for client code.

You should build the whole of Qt (and ideally of some other substantial application based on it, such as Qt Creator) with QT_WARN_DEPRECATED_UP_TO (see below) set to the version in which you're adding your deprecation (or a later version) and submit patches for all modules that get warnings.

If this seems like an overly-burdensome task to you – or, indeed, if you embark on it and it turns out to take several days – then the changes you are asking others to make to their code are a strong argument against going forward with your deprecation.

When deprecating an old API in favor of a new one, it is a kindness to client code maintainers to set the version at which the deprecation takes effect to a future version, such as three minor versions after the new API was added, to give ample time to adapt to it. All the same, you must prepare all of Qt's code for the transition promptly, as if the deprecation took effect as soon as its commit integrated.

As a practical matter, when making deprecations in an upstream module, it is generally prudent to first add the replacement API in the upstream and get it integrated, so that your patches to prepare downstream modules for the deprecation can land, and only later add the deprecations. This can make the process considerably less stressful for everyone involved.

Configuring builds

Two build-time configuration options affect how deprecated Qt APIs are handled. Each of these should be defined to a use of the QT_VERSION_CHECK(major, minor, patch) macro, usually with patch set to 0, identifying a Qt version.

  • Code using things deprecated in the version identified by QT_WARN_DEPRECATED_UP_TO and earlier versions get warnings, unless they are omitted as a result of:
  • Things deprecated in the version identified by QT_DISABLE_DEPRECATED_UP_TO and earlier versions are simply omitted from the API, so code using them will get errors.

So one typically wants the former to identify a later version than the latter. If we think of versions up to and including the former as "old" and those up to and including the latter as "ancient", then our builds simply don't see APIs deprecated in ancient versions, and get warnings about APIs deprecated in any other old versions.

Defaults

If QT_DISABLE_DEPRECATED_UP_TO is not otherwise defined (this is true by default, unless your build system tells the compiler to do so, or you #define it before including any Qt headers), it defaults (at the time of writing, with Qt 6.4 in beta) to QT_VERSION_CHECK(5, 0, 0) and QT_WARN_DEPRECATED_UP_TO defaults to whatever the current Qt version is (as defined, typically via build configuration, in the Qt distribution or source tree), so nothing from present or preceding major version of Qt is omitted from the API (unless it was entirely removed at the major version change) but everything that's deprecated provokes warnings.

If you define QT_DISABLE_DEPRECATED_UP_TO (in the build system, or by a #define before including any Qt headers) then it is used as the default for QT_WARN_DEPRECATED_UP_TO. (This default may change: it has the lamentable result that, when it is exercised, you see no warnings as everything that would be warned about is simply omitted. It can, however, easily be avoided: if you define QT_DISABLE_DEPRECATED_UP_TO, take care to also define QT_WARN_DEPRECATED_UP_TO.)

Building Qt vs building client code

When building Qt itself, the value of QT_DISABLE_DEPRECATED_UP_TO determines what will be left out of Qt. Anything left out in that build will be unavailable to code built against the results. When building client code against such a Qt, QT_DISABLE_DEPRECATED_UP_TO must therefore identify a version no later than the one used when building Qt itself.

There is no such constraint on QT_WARN_DEPRECATED_UP_TO. It is fine to build Qt with more or fewer things warned about than when you build your own code against Qt.

Deprecating an API

When deprecating a private API during a transition, it is usually sufficient to use the QT_DEPRECATED or QT_DEPRECATED_X("message") macro. This, when the compiler supports some mechanism for reporting deprecations, unconditionally triggers such a report for any use of the symbol whose declaration follows any use of this macro. Use of the _X macro is preferred, with the message telling the reader how to replace a use of the deprecated API – or, in really tricky cases, where to find documentation of what to do. This preference applies, likewise, below to the next pair of macros.

For public APIs, direct use of those macros would cause the API to be unconditionally deprecated (without giving any hint to when it'll be removed) from the first version in which the change to the API is released. So instead we use:

  • QT_DEPRECATED_VERSION_X_major_minor("message") before the declaration; this either expands to QT_DEPRECATED_X("message") or to nothing, or
  • QT_DEPRECATED_VERSION_major_minor before the declaration, which expands to either QT_DEPRECATED or nothing.

(In the case of templates, these macros appear between the template preamble and the return type or, where present, static or inline.) If the version identified by the major and minor used with these macros is equal to the one given by QT_WARN_DEPRECATED_UP_TO or older than it, they expand to the active deprecation warning macro, otherwise they expand to nothing.

We also wrap such deprecated API in #if-ery conditioned on QT_DEPRECATED_SINCE(major, minor), which is false if the version identified by major and minor is the one identified by QT_DISABLE_DEPRECATED_UP_TO, or older than this, otherwise true.

The version identified by the QT_DEPRECATED_SINCE(major, minor) on which an API is conditioned should be at least as late as the one identified by the QT_DEPRECATED_VERSION_X_major_minor("message") or QT_DEPRECATED_VERSION_major_minor that precedes the declarations so conditioned.

It is important, when deprecating anything, to include the QT_DEPRECATED-based macro in the declarations. It is not sufficient to merely wrap the declarations in #if-ery on QT_DEPRECATED_SINCE(major, minor). Doing only the latter will cause the declarations to vanish without ever warning maintainers of client code so that they can replace all use of it before it vanishes.

Deprecating enum members

The process for enumeration members is similar to that for other declarations except that, instead of placing QT_DEPRECATED_VERSION_X_major_minor("message") or QT_DEPRECATED_VERSION_major_minor before the declaration, you place Q_DECL_ENUMERATOR_DEPRECATED_X("message") after the name of the member (but before any = value that may be present). The affected member (or members) should still be conditioned on a suitable #if QT_DEPRECATED_SINCE(major, minor) check.

In the documentation, such a member should either be listed in the \enum's list of values using \omitvalue or mention its deprecation in the text of its \value entry.

Implementations and tests

Where an API is wrapped in #if-ery on QT_DEPRECATED_SINCE(major, minor), its implementation must be wrapped in the same. Otherwise, when QT_DISABLE_DEPRECATED_UP_TO is set later than the indicated version, attempts to build Qt will fail due to defining symbols that were not declared. Likewise, for deprecated enum members, code that contains a case to handle the member should be wrapped in #if-ery matching that on the declaration of the deprecated member.

When an API is deprecated, its tests may well simply be converted to use a replacement API. If any test code is retained, that still exercises the deprecated API, it should be wrapped in #if-ery as for the implementation of that API. This shall still provoke warnings for some settings of QT_WARN_DEPRECATED_UP_TO and QT_DISABLE_DEPRECATED_UP_TO.

You can suppress those warnings by placing

QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED

just inside this #if-ery at the start and

QT_WARNING_POP

just inside at the end.