C++ Logo

std-proposals

Advanced search

Re: [std-proposals] DR: concepts std::strict_weak_ordering / std::equivalence_relation must be resticted by semantic requirements

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Sun, 7 May 2023 13:18:26 -0400
On Sun, May 7, 2023 at 11:35 AM Jason McKesson wrote:

> On Sun, May 7, 2023 at 4:11 AM Nikl Kelbon wrote:
> >
> > This compiles:
> > #include <concepts>
> > #include <functional>
> > template<typename A, typename B>
> > bool foo(std::strict_weak_order<A, B> auto) {
> > return true;
> > }
> > template<std::copyable A, typename B>
> > bool foo(std::equivalence_relation<A, B> auto) {
> > return false;
> > }
> > int main() {
> > return foo<int, int>(std::equal_to<>{});
> > }
> > You say i hide ambiguity, but in fact equality of this two concepts now
> hides it.
> > https://godbolt.org/z/bqePq7hcv
>
> The second `foo` has additional constrains the first lacks. The
> standard *does not care* what those constraints mean; what matters is
> that it *has them*. Therefore, there is some set of types `T` which
> the first one can be used with but not the second. Therefore, the
> second is more constrained, and is preferred when applicable.
>

Well, to be fair, I think there is a point here. The compiler certainly
shouldn't treat `std::strict_weak_order` as more constrained than
`std::relation`, but it is still surprising that it treats
`std::strict_weak_order, std::copyable` as more constrained than
`std::equivalence_relation, nothing`.
Consider ( https://godbolt.org/z/6M7KGvbME )

// Rightly ambiguous between one #1 and one #2
void one(int, std::equivalence_relation<int, int> auto) {} // #1
void one(int, std::strict_weak_order<int, int> auto) {} // #2
int main() { one(42, [](int, int){ return false; }); }

// Unambiguous; prefers two #1 over two #2. But this preference has dubious
rationale behind it.
void two(int, std::equivalence_relation<int, int> auto) {} // #1
void two(long, std::strict_weak_order<int, int> auto) {} // #2
int main() { two(42, [](int, int){ return false; }); }

There are two things the programmer might have intended with this second
example.
- The programmer means: "This overload set can take either equivalence
relations (in which case I want #1) or weak orders (in which case I want
#2). The exact type of the integral parameter is less important than, or
equally important as, the relation parameter." The compiler should respond:
"As a compiler, I can't distinguish equivalence relations from weak orders,
so whether #1 or #2 is the better match is unknown at compile time. This is
ambiguous."
- The programmer means: "This overload set takes an integral parameter and
a relation. If the integral parameter is `int`, then the relation is
expected to be an equivalence relation (this is a precondition). If the
integral parameter is `long`, then the relation is expected to be a weak
order (this is a precondition on *that* overload)." The compiler should
respond (and in fact it *does* respond this way in real life): "Ah, okay, I
can't check those preconditions for you, but I'll happily do all my
overload-priority-ranking based on the first parameter's type, and just
assume you got the precondition correct for whichever overload you end up
calling."

Now, I don't immediately see how to *implement* what I'm about to describe.
I think it would probably require feedback between different stages of
deduction/resolution in a really ugly and inelegant (and therefore bad)
way. But leaving aside the C++ aspects for *just* a minute... imagine that
there were some way to mark a pair of concepts as "Deliberately
Ambiguating." Then, when the compiler goes to rank two viable candidates
for overload resolution, it would not only say
- When #1 expects an `A auto`, #2 expects a `B auto`, and A subsumes B,
prefer #1 over #2 (i.e. drop #2 from the ultimate candidate set);
- When #1 expects an `A auto`, #2 expects a `B auto`, and B subsumes A,
prefer #2 over #1 (i.e. drop #1 from the ultimate candidate set);
but also
- When #1 expects an `A auto`, #2 expects a `B auto`, and the concepts
(A,B) are Deliberately Ambiguating, then react the same way you would if a
pair of arguments mutually outranked each other: drop *both* #1 and #2 from
the ultimate candidate set.

This would make `two` above an ambiguous call, and would also make Nikl's
`foo` example an ambiguous call. I think this *might* be more helpful to
the programmer than harmful.
But:
- I'm probably missing some unintended effect where if we did this it would
totally break some Ranges idiom;
- I don't see how to implement this anyway;
- it would require new syntax and might result in a lot of (MxN) busywork
annotations when the programmer tries to express "strict_weak_order,
equivalence_relation, relation are all mutually deliberately ambiguous; my
CustomPredicate concept is deliberately ambiguous with all three of those;
etc. etc."

So I *doubt* this avenue will turn out to be productive, but maybe someone
wants to think about it harder than I have.

–Arthur

Received on 2023-05-07 17:18:39