[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