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

Ivan Solovev ivan.solovev at qt.io
Mon Jul 24 14:34:58 CEST 2023


Hi,

let me try to revive the discussion, because we want the C++20 comparison task
to be finished for Qt 6.7.

> > And we have the calling convention:
> >     lhs.O(rhs);
> >     Qt::O(lhs, rhs); // if you know the type
> >
> >     using Qt::O;
> >     O(lhs, rhs);
> >     ..... // if you don't
>
> I don't think this is necessary. I would prefer if that were only allowed
> through op<=>.
>
> C++17-constrained users would therefore need to know the types in question,
> know what the "O" function is called and how to call it (static vs non-
> static). And if it isn't public at all, then you're limited to the traditional
> relational operators (op<, op<=, op> and op>=). Mind you, we're not breaking
> those users in any way; we're simply not adding extra API for them.

The whole idea of the C++20 comparison epic is to provide a unified comparison
behavior between C++17 and C++20 *and* to provide the three-way-comparison
features to the Qt users who are still stuck with C++17.

As it was discussed in another mailing threads, we are not planning to demand
C++20 until Qt 6.9 ([0] and [1]), and we will also have one more LTS version
(Qt 6.8) based on C++17.

Considering all the above, I think it's important to give our users the
possibility to implement three-way-comparison in their custom classes.
And here the equal() and order() functions would be the building blocks that
we provide for it. In my opinion, this is the main reason to allows things
like
    using Qt::func;
    func(lhs, rhs);

[0]: https://lists.qt-project.org/pipermail/development/2023-May/043812.html
[1]: https://lists.qt-project.org/pipermail/development/2023-May/043865.html

Now, regarding the helper function naming:

> Why doesn't "equals" work?
>
> We've been over this in the code review: our current naming conventions would
> dictate "equals" even for two arguments, as the most common phrasing is "A
> equals B" instead of "A and B equal". When you compare things, you always
> compare at a minimum two of them, so there being two is implied by the act of
> comparing anyway. And I don't see why we can't provide both static and non-
> static members, like QString::compare does

IIUC, the main problem that Marc has in mind is that the existing class member
equals() would conflict with the two-arg hidden friend equals(), which we would
use as a helper for operator==() and operator!=().

Grepping through all Qt sources, I see quite a lot of usages of one- or two-arg
equals() methods, however most of them are private.
The private methods are not an issue, because we can use
QT_<MODULE>_REMOVED_SINCE to get rid of them.

The public methods, in turn, are actually an issue, because the compiler will
try to use them instead of a defined hidden friend, even if the number of
arguments or their types do not match.
See here for an example:
    https://godbolt.org/z/qW6c9dPY8 (two-arg public equals)
    https://godbolt.org/z/d19qvxv79 (one-arg public equals)

However, I managed to make it work introducing an extra private static method:
    https://godbolt.org/z/3Ycf5Kh1Y (two-arg public equals)
    https://godbolt.org/z/nv9jK4eqv (one-arg public equals)

This is not an ideal solution, because, as you can see in the output, the
operator==() explicitly calls the private static method instead of the hidden
friend. However, the generic code also works (this time calling hidden friend,
which forwards to private static).

Probably this approach would allow us to use "equals()" as a name for the E
function.
What do you think?

Best regards,

------------------------------

Ivan Solovev
Senior Software Engineer

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

Geschäftsführer: Mika Pälsi,
Juha Varelius, Jouni Lintunen
Sitz der Gesellschaft: Berlin,
Registergericht: Amtsgericht
Charlottenburg, HRB 144331 B
________________________________
From: Development <development-bounces at qt-project.org> on behalf of Thiago Macieira <thiago.macieira at intel.com>
Sent: Wednesday, June 14, 2023 6:16 PM
To: development at qt-project.org <development at qt-project.org>
Subject: Re: [Development] C++20 comparisons @ Qt (was: Re: C++20 @ Qt)

On Wednesday, 14 June 2023 01:52:49 PDT Marc Mutz via Development wrote:
> == 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).

It's also why glibc added __memcmpeq and GCC and Clang can generate a call to
that. Same reason why I added QtPrivate::equalStrings() different from
QtPrivate::compareStrings() (internally they call ucstreq and ucstrcmp,
respectively).

> == 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).

Hmm... the facts are correct but I don't think that leads to the conclusion.
The whole objective here is to provide op== anyway, so the fact that some
classes have this implemented in another method that can do more should be
irrelevant. For those, this op== can be inline and call the out-of-line
function that does the relevant work.

> 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,
> ...).

They can be exceptions and written manually.

However, I do not object to having the op== be an inline hidden friend,
calling a private static noexcept equality comparator function (whether that's
inline or not, taking extra arguments or not). In fact, I like that, purely on
stylistic reasons.

> == 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.

Side note: I think we should start compiling Qt with C++20 right now, for all
compilers that support it, with no possibility for the user to turn that off.
There is C++20 functionality we could use in our implementations that don't
affect ABI and don't constrain C++17 users. And I also don't object to
providing C++20-only features, discussed on a case-by-case basis.

> Applied to our situation, this means:
>
> - each (orderable) class supplies O as a hidden friend
> - may supply it as a (non-static) member function

"At a minimum" as a hidden friend. It may be public API, like
QString::compare(), whether static or non-static.

> And we have the calling convention:
>     lhs.O(rhs);
>     Qt::O(lhs, rhs); // if you know the type
>
>     using Qt::O;
>     O(lhs, rhs);
>     ..... // if you don't

I don't think this is necessary. I would prefer if that were only allowed
through op<=>.

C++17-constrained users would therefore need to know the types in question,
know what the "O" function is called and how to call it (static vs non-
static). And if it isn't public at all, then you're limited to the traditional
relational operators (op<, op<=, op> and op>=). Mind you, we're not breaking
those users in any way; we're simply not adding extra API for them.

Paraphrasing you about a decade ago, "C++17 costs more".

> 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.

Conclusion invalidated due to premise rejected. I couldn't follow your logic
anyway, but since the premise was no longer applicable, I didn't try very
hard.

> == Naming O ==
>
> Given that compare() doesn't work, what are alternatives?

Moot section.

> == 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).

Why doesn't "equals" work?

We've been over this in the code review: our current naming conventions would
dictate "equals" even for two arguments, as the most common phrasing is "A
equals B" instead of "A and B equal". When you compare things, you always
compare at a minimum two of them, so there being two is implied by the act of
comparing anyway. And I don't see why we can't provide both static and non-
static members, like QString::compare does

I agree "areEqual" is ugly.

> 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 <=> :)

Too short to be public API. Those could only be implementation details and
therefore need not be discussed.

--
Thiago Macieira - thiago.macieira (AT) intel.com
  Cloud Software Architect - Intel DCAI Cloud Engineering
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.qt-project.org/pipermail/development/attachments/20230724/7fe5067d/attachment-0001.htm>


More information about the Development mailing list