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

Thiago Macieira thiago.macieira at intel.com
Wed Jun 14 18:16:32 CEST 2023


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 --------------
A non-text attachment was scrubbed...
Name: smime.p7s
Type: application/pkcs7-signature
Size: 5152 bytes
Desc: not available
URL: <http://lists.qt-project.org/pipermail/development/attachments/20230614/9c010433/attachment.bin>


More information about the Development mailing list