C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Let spaceship return an int

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Wed, 27 Sep 2023 03:11:55 -0400
On Tue, Sep 26, 2023 at 9:40 PM Chris Gary via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> Its hard for me to say what inspired many of the responses I received, aside from my clicking "reply to" and having a good bit of unproductive discussion vanish into personal conversations with others. Yet, here, it is not having problems putting your email on the CC list, and the message board on the main...
>
> We have a set of 3 things in std::strong_ordering, and an implicit 4th that is "not defined" or "unspecified". "Unspecified" is not an error, its just UB, which we've all come to know and work with. Proving that an arbitrary operator <=>, return type notwithstanding, will never result in UB is ultimately undecidable (also obvious).

`strong_ordering` only permits 3 possible values; it has no "not
defined" or "unspecified" value. Only `partial_ordering` allows 4
values, and `partial_ordering` is not convertible to
`strong_ordering`.

This is part of why `float`-based `operator<=>` will return
`std::partial_ordering`.

> Wherever a user-defined operator <=> suddenly produces an int, the compiler can instead insert code to check against any of {-1,0,1} depending on what was synthesized.

And what about if you pass that `int` to some other function instead
of using it locally? A function that is not inline and therefore there
is no way for the compiler to know that the `int` value it receives
magically carries semantic information distinct from other `int`
values.

So that's just not a viable solution.

If you want to have values that carry special semantic information
distinct from other things, the standard way to do that in C++ is to
have those values be of a different type from other values. This
allows that semantic information to be non-local, to carry to whomever
gets the value no matter how they get that value.

That's what types are for. Like, carrying semantic information beyond
the bag of bits used to store it is one of the most fundamental
reasons why strong typing *exists*.

You *could* remove `float` from the language and just have special
operations on `int` that treat it as an IEEE-754 BINARY32 floating
point value. But like... why? Why would you even want that? Why would
you want to throw away semantic information that is vital to
understanding what the code is actually doing?

Because you don't like including a header?

Also, why do you insist on limiting it to the [-1, 1] range? If you
don't want semantic information carried in the type, if all semantics
is to be implicit based on its use, why not allow the user to return
whatever they want?

> No need for a header or privileged symbols in namespace std. Again, I proposed this assuming a good deal of other things were implied.
>
> The comment about "Hacking into Clang..." was meant for another thread, the discussion in which seemed to creep into this one. At least a mention of "polyfill" was made in one post I responded to, and that ended up here.
>
> This won't break or interact with modules as far as I can tell. In general code, obviously something like decltype( a <=> b ), but then wherever that is being done, it probably doesn't care about specific names...
>
> To every complaint about "ABI breakage" etc... It won't break any existing code to allow new code to define an int-valued <=>. Its just a function by another name. The behavior of the compiler can be made well-defined in either case.

It will make that code non-interoperable with code that uses
semantic-carrying return types. Or at the very least, such code would
be required to assume the worst case (partial ordering). But even if
this were adopted, I would suggest that every standard library type
that interacts with a user-provided `T`'s comparison operator
`static_assert` on that comparison operator, should it not return a
semantic-carrying type.

This at least would prevent people from accidentally using this
"feature" in reasonable code.

> The comment in the opening about "all knowledge being integers" etc... also obvious, was meant to set the tone to "this is always possible, reasoning to the contrary must now confront an obvious truth." Why? I feel the decision to impose semantics through a return type is bad design, and thought the reasoning for its adoption was due to a narrow scope of understanding.

Well, I feel that opinion is categorically incorrect. And the rest of
the language seems to agree with me more than yourself, since it does
this kind of thing all the time. Again, `float` is technically
32-bits, just like `int`, so everything special about it is just
"semantics". But it has a distinct type. `pair<float, float>` is
conceptually equivalent to `std::complex<float>`, but they are
different types. Why?

Because that's *what types are for*. The entire point of having types
exist as a concept is to impose semantics upon raw values.
`unique_ptr<T>` is just a set of semantics imposed on a pointer.
Should code not return `unique_ptr<T>`s either?

The thing about semantic information is this: there is frequently
little benefit to *not* having it in the type system. If you
semantically need to delete a pointer at the end of scope, there is
basically no upside to using a raw pointer instead of `unique_ptr`.
But there are *downsides* to using the type that doesn't carry the
semantic information. You have to maintain that semantic information
in your own head. And the heads of the people reading the code. It
makes code more brittle and less readable.

So besides your own personal preference against including standard
library headers, I do not see any benefit to avoiding the C++
comparison types. And I don't find your value statement of "impose
semantics through a return type is bad design" to be a viable
argument.

Received on 2023-09-27 07:12:07