[Development] C++20 comparisons @ Qt (was: Re: C++20 @ Qt)

Marc Mutz marc.mutz at qt.io
Wed Jun 14 10:52:49 CEST 2023


On 03.11.22 10:40, Marc Mutz via Development wrote:
> 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 part somehow became lost and kinda made the whole feature miss the 
6.6 train, so let me expand on it a bit, and collect open questions 
(marked with → below):

We want the implementation of these relational operators be as close as 
possible to the way C++20 will work: There, you implement op== and op<=> 
and the compiler synthesizes everything else (cf. parent message for 
details). So we need one function E backing op== and, for ordered types, 
another function O backing op<=>.

== Why E _and_ O? ==

The reason we need E _and_ O for ordered types instead of just O is that 
O needs to order the lhs w.r.t. the rhs, which generally involves 
looking at all the state of the objects whereas E just needs to find 
_one_ difference to be able to quickly return false. E.g., a container 
can do

     bool E(~~~ lhs, ~~~ rhs) {
         if (lhs.size() != rhs.size()) return false; // O cannot do this!
         ~~~~
     }

This is a very important optimization (and one reason why L1 string 
literals are going to stay and not be replaced with UTF-8 ones: L1 op 
UTF-16 _can_ use the size() short-cut while UTF-8 op UTF-16 cannot).

== Can E be spelled op==? ==

It could. However, op== cannot have extra arguments and we have at least 
one use-case where E might have an additional argument: case-insensitive 
string comparisons. We currently have no (public) API to express op==, 
but case-insensitively. The closest we have is QString::compare(), but 
that is an O, so it cannot use the size() mismatch shortcut, assuming 
it's valid for case-insensitive string comparison (it is for L1, dunno 
about UTF-16).

So there's a certain appeal in using E as a way to consistently spell 
"op== with extra arguments". We could, e.g. do E(lhs, rhs, 
Qt::FuzzyComparison) for anything involving FP.

There's also the situation where (cheap!) implicit conversions allow one 
E to backfill several different op== (which, being hidden friends, don't 
participate in implicit conversions themselves). If E is spelled op==, 
what would the macros use to implement op==? It would be up to the class 
author to supply sufficiently-many op== in the correct form. We don't 
want that. We want all op== to be generated from the macros so we have 
central control over their generation (providing reversed operators in 
C++17, getting the signatures right, noexcept-ness, hidden-friend-ness, 
...).

So I would require new information to accept anything than a "no" here:

→ Semi-Open question 1:
Should E be spelled op==?

My take is no.

→ Open question 2:
Should E exist for all types, as a "concept name" for "op== with 
parameters", or should we leave that, as it is now, on type-by-type 
basis to each class author individually?

My take is that yes, it should exist as a "concept name". I added 
qStringsEqual() in 5.10 as public API when adding QStringView, but it 
was relegated to private API before the release. When class authors are 
given the option to accept parameters for equality comparison, they will 
find novel ways to use it (like Qt::Fuzzy).

== O as public API ==

In C++20, O would be spelled op<=>, but this is not possible in C++17, 
so O needs to be an actual named function, not an operator. The 
question, and it's a rhetorical one, then becomes: should O be public 
API, and the (rhetorical) answer is "yes, because C++17 users will want 
to be able to access O in C++17 projects, too (in C++20, they could call 
op<=>)". This includes Qt's own implementation of O's.

The gold standard for named operators in C++ is to have the 
implementation as a hidden friend (think swap()), possibly a member 
function (lhs.swap(rhs)), a (namespaced) fallback version for built-in 
types (std::swap(int, int)) and a calling convention that takes this 
into account:

    lhs.swap(rhs);
    // or
    std::swap(lhs, rhs); // if you know decltype(lhs), ...

    using std::swap;
    swap(lhs, rhs);
    // or
    std::ranges::swap(lhs, rhs); // ... if you don't.

Applied to our situation, this means:

- each (orderable) class supplies O as a hidden friend
- may supply it as a (non-static) member function
- Qt provides an implementation for built-in types in a namespace (we 
only have 'Qt').

And we have the calling convention:

    lhs.O(rhs);
    Qt::O(lhs, rhs); // if you know the type

    using Qt::O;
    O(lhs, rhs);
    O_as_CPO(lhs, rhs); // if you don't

O_as_CPO is to O what std::ranges::swap is to swapping: an ADL-enabled 
version that allows to skip the using Qt::O/using std::swap;

The same would be applicable to E if Open Question 1 ends up resolved as 
no and Open Question 2 as "yes, named concept".

This leads to the finding that O (and, depending on the outcome of the 
first two Open Questions) E cannot be named like static binary functions 
in existing classes, as, even if they are semantically compatible, 
they're syntactically incompatible.

That objectively rules out "compare" for O and "equals" for E.

== Naming O ==

Given that compare() doesn't work, what are alternatives? In the patch 
series so far, we've been using order(), as the operation orders the two 
arguments (and returns an ordering type). An alternative that was 
discussed was ordering(), which I'd be ok with, and qCompare(), which I 
would reserve as a CPO (ie. as O_as_CPO), if we don't use qOrder or 
qOrdering for that.

→ Open Question 3:
What should we call O?

My take: After contemplating this for the last two weeks: cmp() (1st), 
order() (2nd) or ordering() (3rd). I really think it pays for these 
function names to be _short_. Size _does_ matter, having very common 
functions be one word and therefore visually distinct from 
multiWordIdentifiers helps readability. Bjarne is right: people ask for 
verbose syntax for new features, so they stand out, then the whole 
ecosystem suffers from ugliness for decades to come.

== Naming E ==

So far, we've been using equal(). equals() doesn't work for technical 
reasons, but while it'd work as a member function lhs.equals(rhs), it's 
also kinda wrong if the function is taking two arguments (equals(lhs, 
rhs), but there are _two_ objects). So equal() as the plural form or 
equals() makes sense. There were also proposals of qEqual() (which I'd 
again reserve as the CPO name of E) and areEqual() (which I find uglily 
long).

→ Open Question 4:
What, if anything, should be call E?

My take: eq() (1st), equal() (2nd). I _really_ don't like areEqual() for 
the reasons given.

order() and equal() have the nice property of being equally long. With 
equal() typically returning bool and order() often returning auto, this 
makes for a very pleasing alignment of the two functions. But after 
thinking about this long and hard, I think eq() and cmp() are best, esp. 
if you consider that they stand for == and <=> :)

Thanks,
Marc

-- 
Marc Mutz <marc.mutz at qt.io>
Principal Software Engineer

The Qt Company
Erich-Thilo-Str. 10 12489
Berlin, Germany
www.qt.io

Geschäftsführer: Mika Pälsi, Juha Varelius, Jouni Lintunen
Sitz der Gesellschaft: Berlin,
Registergericht: Amtsgericht Charlottenburg,
HRB 144331 B



More information about the Development mailing list