[Development] C++20 comparisons @ Qt (was: Re: C++20 @ Qt)
Marc Mutz
marc.mutz at qt.io
Thu Nov 3 10:40:51 CET 2022
Hi,
C++20 changed the semantics of relational operators quite a bit, and it
affects all Qt classes that are equality-comparable and/or ordered.
In the following, A and B are types and a and b are objects of type A
and B, resp.
First, the compiler will now synthesize missing operators from existing
ones. So a C++20 program only needs to provide op==(A, B) and the
compiler can use this to satisfy requests for all of these:
- a == b (obviously)
- a != b (by inverting the result)
- b == a (by reversing the arguments)
- b != a (by doing both)
In turn, this can now cause ambiguities (ie. hard compile errors) when
there's an asymmetry between user-provided operators and the
compiler-synthesized ones. At the very least, we need to fix these for
our C++20 users and one open question is whether we pick these into
older branches and if so, how far back. I know projects that were very
close to using C++20 together with Qt 5 a year or so ago.
One can even now = default the equality operator. Then the compiler
creates one based on for member-wise equality. If all such members have
an = default'ed equality operator, and with a few other other
requirements, the type in question has Strong Structural Equality, and
thus can be used as arguments to non-type template parameters (NTTP).
QFlags and QFixed are examples where this might come in handy. There are
probably others, too.
For ordered types, C++20 introduces a new relational operator,
affectionately called the spaceship operator <=>. It basically returns
-1, 0, 1 based on whether the lhs is <, ==, > the rhs (think strcmp).
However, it doesn't return int, but one of three scoped enums:
- partial_ordering (allows incomparable elements, think NaN op NaN, if
we could turn back time and rewrite IEEE754)
- weak_ordering (allows equivalent, but non-equal elements, think qstricmp)
- strong_ordering (requires a trichotomy (exactly one of <, ==, > is
true for every pair or lhs, rhs and there's no public API that, when ==,
allows to tell a difference between the types (addressof is allowed to
differ)
From this operator,, the compiler can synthesize all of <, >, <=, >=. But users
can also call it manually.
So, not only must we provide op <=> for all of our ordered types, we
must also decide in which of the three categories (partial, weak,
strong) each comparison falls. And not just for homogeneous relations (a
op a), but heterogeneous ones, too (a op b).
I think this presents us with an excellent opportunity to re-design our
currently-ad-hoc way of dealing with relational operators, which all too
often are just documented as functions (documenting individual op== op!=
for all combinations of {QRect,QRectF} × {QRect,QRectF} instead of as
properties of the type (QRect: "QRect is equality-comparable with itself
and QRectF", QRectF: "QRectF is equality-comparable with itself and QRect").
I think this would improve the user documentation immensely, esp. if we
find a way to conveniently say why some relation is _not_ provided, too
("QRect is not ordered, because any ordering would be arbitrary and not
natural") and = delete the corresponding operators:
// QRect:
friend void operator<=>(QRect, QRect) = delete;
// QRectF:
friend void operator<=>(QRectF, QRectF) = delete;
friend void operator<=>(QRectF, QRect) = delete;
I have drafted a proposal for how to go about the change here:
https://bugreports.qt.io/browse/QTBUG-103757?focusedCommentId=671497#comment-671497
TL;DR: provide named functions for the minimal set of op== and op<=>
we'd have to write in C++20, then use macros to turn these into op==,
op!=, op<, op>, op<=, op>= for C++17 builds and op== and op<=> for C++20
builds.
This would be much easier done with an ABI break, but we have the tools
(QT_REMOVED_SINCE) now to deal with this already now.
If we also make all these operators hidden friends
(https://bugreports.qt.io/browse/QTBUG-87973), then we at the same time
improve the user experience by removing overly-verbose error messages
when something goes wrong. If we do this, though, some incidental
comparisons that rely on implicit conversions will stop working. This is
a good thing, IMO, because it forces us to provide (and therefore
document) these operators explicitly.
The process will also force us to (at least mentally) assign level
numbers to heterogeneously-comparable classes. This might run into
problems (such as QTBUG-108136), but is overall a good thing.
Opinions? Comments?
Thanks,
Marc
References:
- https://bugreports.qt.io/browse/QTBUG-103757
- https://bugreports.qt.io/browse/QTBUG-104110
- https://bugreports.qt.io/browse/QTBUG-104114
- https://bugreports.qt.io/browse/QTBUG-104108
- https://en.cppreference.com/w/cpp/header/compare
- https://bugreports.qt.io/browse/QTBUG-104180
More information about the Development
mailing list