Shortcut "&&" operator to chain <=> "spaceship" comparisons (at least for strong_ordering)

253 views
Skip to first unread message

charle...@gmail.com

unread,
Dec 4, 2017, 5:48:21 PM12/4/17
to ISO C++ Standard - Future Proposals
This paper:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0515r0.pdf
proposes a new operator, "operator <=>", to do 3-way comparisons.

It gives this example as a potential implementation of an operator<=>:

std::strong_ordering operator<=> (const TotallyOrdered & that) const
{
    if (auto cmp = (Base&) (*this) <=> (Base&)that; cmp != 0) return cmp;
    if (auto cmp = last_name <=> that.last_name; cmp != 0) return cmp;
    if (auto cmp = first_name <=> that.first_name; cmp != 0) return cmp;
    return tax_id <=> that.tax_id;
}

A comparable operator== would be written this way:

bool operator== (const TotallyOrdered & that) const
{
   return (Base&) (*this) == (Base&)that
   && last_name == that.last_name
   && first_name == that.first_name
   && tax_id == that.tax_id;
}

So, my suggestion is to add semantics for "&&" on the "strong_ordering" type (and "weak_ordering" which is works effectively the same way) such that
a && b
evaluates to "a", without evaluating "b", if "a != 0". Otherwise, "b" is evaluated, and its value is the value of the expression.
This cannot be done with regular overloading, as there is significant value in the shortcut behavior.
This way, the overloaded operator<=> can be written this way:
std::strong_ordering operator<=> (const TotallyOrdered & that) const
{
   return (Base&) (*this) <=> (Base&)that
   && last_name <=> that.last_name
   && first_name <=> that.first_name
   && tax_id <=> that.tax_id;
}
which seems like a worthwhile improvement.

Nevin Liber

unread,
Dec 4, 2017, 6:13:58 PM12/4/17
to std-pr...@isocpp.org
On Mon, Dec 4, 2017 at 4:48 PM, <charle...@gmail.com> wrote:
This paper:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0515r0.pdf
proposes a new operator, "operator <=>", to do 3-way comparisons.

It gives this example as a potential implementation of an operator<=>:

std::strong_ordering operator<=> (const TotallyOrdered & that) const
{
    if (auto cmp = (Base&) (*this) <=> (Base&)that; cmp != 0) return cmp;
    if (auto cmp = last_name <=> that.last_name; cmp != 0) return cmp;
    if (auto cmp = first_name <=> that.first_name; cmp != 0) return cmp;
    return tax_id <=> that.tax_id;
}

A comparable operator== would be written this way:

bool operator== (const TotallyOrdered & that) const
{
   return (Base&) (*this) == (Base&)that
   && last_name == that.last_name
   && first_name == that.first_name
   && tax_id == that.tax_id;
}

So, my suggestion is to add semantics for "&&" on the "strong_ordering" type (and "weak_ordering" which is works effectively the same way) such that
a && b
evaluates to "a", without evaluating "b", if "a != 0". Otherwise, "b" is evaluated, and its value is the value of the expression.
This cannot be done with regular overloading, as there is significant value in the shortcut behavior.

The return statements in the code generated by <=> is equivalent to short circuiting, isn't it?  I don't see the purpose of special casing here.  Could you elaborate?
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com>  +1-847-691-1404

Todd Fleming

unread,
Dec 4, 2017, 6:36:09 PM12/4/17
to ISO C++ Standard - Future Proposals
The short circuiting on weak_ordering example seems less wordy to me; it has no ifs and fewer returns. If I was doing a code review I could more quickly see that it was correct.

Todd

Nicol Bolas

unread,
Dec 4, 2017, 6:42:50 PM12/4/17
to ISO C++ Standard - Future Proposals
Which is not especially relevant, since you usually don't have to write `<=>` yourself.

I don't like the idea of adding special semantics to an existing operator that when applied to specific types. If we want to add an operator that has works like this on all types (which are comparable to 0), that's one thing. Or if we want to add a way for people to write such conditional statements, that's one thing.

But to make `&&` work differently from how it does now when applied to a standard library type? No.

Thiago Macieira

unread,
Dec 4, 2017, 7:05:51 PM12/4/17
to std-pr...@isocpp.org
On Monday, 4 December 2017 14:48:21 PST charle...@gmail.com wrote:
> So, my suggestion is to add semantics for "&&" on the "strong_ordering"
> type (and "weak_ordering" which is works effectively the same way) such that
> a && b
> evaluates to "a", without evaluating "b", if "a != 0".

So you want to change && when left and right are ints?

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

charle...@gmail.com

unread,
Dec 4, 2017, 7:23:09 PM12/4/17
to ISO C++ Standard - Future Proposals
This remains to be seen. I suspect that manual operator<=> is still going to be relatively common (obiously this is pretty speculative)
 
I don't like the idea of adding special semantics to an existing operator that when applied to specific types. If we want to add an operator that has works like this on all types (which are comparable to 0), that's one thing. Or if we want to add a way for people to write such conditional statements, that's one thing.
It already has special semantics on "bool".


charle...@gmail.com

unread,
Dec 4, 2017, 7:26:21 PM12/4/17
to ISO C++ Standard - Future Proposals

On Monday, December 4, 2017 at 4:05:51 PM UTC-8, Thiago Macieira wrote:
On Monday, 4 December 2017 14:48:21 PST charle...@gmail.com wrote:
> So, my suggestion is to add semantics for "&&" on the "strong_ordering"
> type (and "weak_ordering" which is works effectively the same way) such that
> a && b
> evaluates to "a", without evaluating "b", if "a != 0".

So you want to change && when left and right are ints?
No. Only if they're both "strong_ordering" or both "weak_ordering". These have semantics that allow "!= 0" to mean something. It's probably equivalent to "== <some enumerator>" but it's usually expressed that way in the paper (e.g. the example I give). I am not suggesting changing the semantics of anything that already exists in the standard.

Richard Smith

unread,
Dec 4, 2017, 7:32:53 PM12/4/17
to std-pr...@isocpp.org
Giving the built-in && a return type other than "bool" for some operands seems very surprising to me.

Perhaps it's time to consider the GNU binary conditional operator again?

  return a.x <=> b.x ?: a.y <=> b.y ?: a.z <=> b.z;

... seems to be exactly what we want (assuming that contextual conversion of the comparison category types to bool actually works, which I've been assuming is the case but haven't checked...).

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/0a442019-a580-4d39-80ed-0890e2ceb3e8%40isocpp.org.

charle...@gmail.com

unread,
Dec 4, 2017, 7:32:59 PM12/4/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com
(as opposed to any overloaded version)

Nicol Bolas

unread,
Dec 4, 2017, 7:34:15 PM12/4/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com
Really? Because I don't imagine a lot of circumstances where I would do that. Not for homogeneous comparisons. Even if there were some members that aren't meant to be compared during such an operation, I'd likely aggregate all the comparable members into a struct and compare the comparable structs to each other. That is, `operator<=>` would just forward to `comparable_member::operator<=>`, which is defaulted.

For heterogeneous comparisons, yes, you'll need to write that.

I don't like the idea of adding special semantics to an existing operator that when applied to specific types. If we want to add an operator that has works like this on all types (which are comparable to 0), that's one thing. Or if we want to add a way for people to write such conditional statements, that's one thing.
It already has special semantics on "bool".

Sure, and wouldn't it be so much better if we could get those special semantics on our types that are boolean-like?

Nicol Bolas

unread,
Dec 4, 2017, 7:36:53 PM12/4/17
to ISO C++ Standard - Future Proposals


On Monday, December 4, 2017 at 7:32:53 PM UTC-5, Richard Smith wrote:
Giving the built-in && a return type other than "bool" for some operands seems very surprising to me.

Perhaps it's time to consider the GNU binary conditional operator again?

  return a.x <=> b.x ?: a.y <=> b.y ?: a.z <=> b.z;

First Spaceships, now Elvis.

BTW, I completely approve of this escalation ;)
 

charle...@gmail.com

unread,
Dec 4, 2017, 7:44:57 PM12/4/17
to ISO C++ Standard - Future Proposals


On Monday, December 4, 2017 at 4:32:53 PM UTC-8, Richard Smith wrote:
Giving the built-in && a return type other than "bool" for some operands seems very surprising to me.

Perhaps it's time to consider the GNU binary conditional operator again?

  return a.x <=> b.x ?: a.y <=> b.y ?: a.z <=> b.z;

... seems to be exactly what we want (assuming that contextual conversion of the comparison category types to bool actually works, which I've been assuming is the case but haven't checked...).
No those types are not convertible to bool and that's a feature.
You don't want to accidentally write "if(a <=> b)" instead of "if((a <=> b) == 0)"

charle...@gmail.com

unread,
Dec 4, 2017, 7:50:23 PM12/4/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com
In my own code, it's not uncommon to have members that are not compared (e.g. cached result), or that are only compared based on extra parameters to comparison function.

For heterogeneous comparisons, yes, you'll need to write that.

I don't like the idea of adding special semantics to an existing operator that when applied to specific types. If we want to add an operator that has works like this on all types (which are comparable to 0), that's one thing. Or if we want to add a way for people to write such conditional statements, that's one thing.
It already has special semantics on "bool".

Sure, and wouldn't it be so much better if we could get those special semantics on our types that are boolean-like?

Yes it would, of course. But that would be a big change. I'm proposing a small change, and it would not hamper the bigger change you're suggesting.

Richard Smith

unread,
Dec 4, 2017, 8:26:20 PM12/4/17
to std-pr...@isocpp.org
You don't want to write the latter ever, though. It should be written "if (a == b)". (If that means something other than "(a <=> b) == 0", your type is broken.) Are there other examples where an explicit conversion to bool for these types would be dangerous? It would clearly be useful, not just for the above new operator ?: but also for:

  if (auto cmp = a.x <=> b.x) return cmp;
  if (auto cmp = a.y <=> b.y) return cmp;
  if (auto cmp = a.z <=> b.z) return cmp;

Thiago Macieira

unread,
Dec 4, 2017, 9:33:03 PM12/4/17
to std-pr...@isocpp.org
On Monday, 4 December 2017 16:26:20 PST charle...@gmail.com wrote:
> On Monday, December 4, 2017 at 4:05:51 PM UTC-8, Thiago Macieira wrote:
> > On Monday, 4 December 2017 14:48:21 PST charle...@gmail.com <javascript:>
> >
> > wrote:
> > > So, my suggestion is to add semantics for "&&" on the "strong_ordering"
> > > type (and "weak_ordering" which is works effectively the same way) such
> >
> > that
> >
> > > a && b
> > > evaluates to "a", without evaluating "b", if "a != 0".
> >
> > So you want to change && when left and right are ints?
>
> No. Only if they're both "strong_ordering" or both "weak_ordering".

And what are strong_ordering or weak_ordering?

If they are enums, then they behave like int. Special-casing a type seems like
a bad idea.

If they are a class, then you can collect references to all types and execute
the comparison at the moemnt of conversion to int.

> These
> have semantics that allow "!= 0" to mean something. It's probably
> equivalent to "== <some enumerator>" but it's usually expressed that way in
> the paper (e.g. the example I give). I am not suggesting changing the
> semantics of anything that already exists in the standard.

Sorry, but you kind of are.

charle...@gmail.com

unread,
Dec 4, 2017, 9:40:49 PM12/4/17
to ISO C++ Standard - Future Proposals
It's true that the automatic generation of "==" from "<=>" means that the "strcmp error" is less likely, and boolean conversion potentially useful. I do not believe it is proposed in the original paper. The fact that "if(a <=> b)" would be equivalent to "if(a != b)" is a little weird in my opinion but the benefits may be worth it.

Thiago Macieira

unread,
Dec 4, 2017, 9:43:30 PM12/4/17
to std-pr...@isocpp.org
On Monday, 4 December 2017 18:40:49 PST charle...@gmail.com wrote:
> It's true that the automatic generation of "==" from "<=>" means that the
> "strcmp error" is less likely, and boolean conversion potentially useful. I
> do not believe it is proposed in the original paper. The fact that "if(a
> <=> b)" would be equivalent to "if(a != b)" is a little weird in my opinion
> but the benefits may be worth it.

There are some languages that used <> as the "not equal" sign. <=> is similar
enough.

charle...@gmail.com

unread,
Dec 4, 2017, 9:45:05 PM12/4/17
to ISO C++ Standard - Future Proposals


On Monday, December 4, 2017 at 6:33:03 PM UTC-8, Thiago Macieira wrote:
On Monday, 4 December 2017 16:26:20 PST charle...@gmail.com wrote:
> On Monday, December 4, 2017 at 4:05:51 PM UTC-8, Thiago Macieira wrote:
> > On Monday, 4 December 2017 14:48:21 PST charle...@gmail.com <javascript:>
> >
> > wrote:
> > > So, my suggestion is to add semantics for "&&" on the "strong_ordering"
> > > type (and "weak_ordering" which is works effectively the same way) such
> >
> > that
> >
> > > a && b
> > > evaluates to "a", without evaluating "b", if "a != 0".
> >
> > So you want to change && when left and right are ints?
>
> No. Only if they're both "strong_ordering" or both "weak_ordering".

And what are strong_ordering or weak_ordering?

They are whatever they need to be to make the semantics work. The example implementation uses classes.


If they are enums, then they behave like int. Special-casing a type seems like
a bad idea.

If they are a class, then you can collect references to all types and execute
the comparison at the moemnt of conversion to int.

I do not understand what you mean by this. How do you avoid computing the class value using shortcut semantics?
e.g.
if(a <=> b && expensiveToCompute() <=> c) {
}
how do you avoid running "expensiveToCompute()"? This might even be a correctness issue, it could be "crashIfANotEqualToB()"



> These
> have semantics that allow "!= 0" to mean something. It's probably
> equivalent to "== <some enumerator>" but it's usually expressed that way in
> the paper (e.g. the example I give). I am not suggesting changing the
> semantics of anything that already exists in the standard.

Sorry, but you kind of are.
Am not :)

Nicol Bolas

unread,
Dec 4, 2017, 10:16:04 PM12/4/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com
In the case of the latter... what does it matter? You can't pass "extra parameters" to `operator<=>`.

And in the case of the former, I explained how to make that easy: put the comparable members in their own type. This has the advantage of being much more maintainable, since if you add a new member to the comparison structure, it automatically gets compared.


For heterogeneous comparisons, yes, you'll need to write that.

I don't like the idea of adding special semantics to an existing operator that when applied to specific types. If we want to add an operator that has works like this on all types (which are comparable to 0), that's one thing. Or if we want to add a way for people to write such conditional statements, that's one thing.
It already has special semantics on "bool".

Sure, and wouldn't it be so much better if we could get those special semantics on our types that are boolean-like?

Yes it would, of course. But that would be a big change. I'm proposing a small change, and it would not hamper the bigger change you're suggesting.

I'd rather there be no change than a change like this. It confuses the language to have an existing operator work in a completely different way when used with certain specific types, which you are unable to write.


Thiago Macieira

unread,
Dec 4, 2017, 10:19:35 PM12/4/17
to std-pr...@isocpp.org
On Monday, 4 December 2017 18:45:05 PST charle...@gmail.com wrote:
> > If they are a class, then you can collect references to all types and
> > execute
> > the comparison at the moemnt of conversion to int.
>
> I do not understand what you mean by this. How do you avoid computing the
> class value using shortcut semantics?
> e.g.
> if(a <=> b && expensiveToCompute() <=> c) {
> }
> how do you avoid running "expensiveToCompute()"? This might even be a
> correctness issue, it could be "crashIfANotEqualToB()"

By delaying the evaluation.

Instead of operator<=> performing the comparison, it returns an object that
has a reference to the left and right sides. Then operator&& just collects the
results. At the end of the statement, the conversion operator, move
constructor, or another function performs the actual comparisons, with short-
cuircuiting if necessary.

> > > These
> > > have semantics that allow "!= 0" to mean something. It's probably
> > > equivalent to "== <some enumerator>" but it's usually expressed that way
> >
> > in
> >
> > > the paper (e.g. the example I give). I am not suggesting changing the
> > > semantics of anything that already exists in the standard.
> >
> > Sorry, but you kind of are.
>
> Am not :)

You should write a paper that provides for a way to create custom operator&&
and operator|| that allow for short-circuiting. That would be useful in other
cases besides operator<=>.

Nicol Bolas

unread,
Dec 4, 2017, 10:27:30 PM12/4/17
to ISO C++ Standard - Future Proposals
On Monday, December 4, 2017 at 10:19:35 PM UTC-5, Thiago Macieira wrote:
On Monday, 4 December 2017 18:45:05 PST charle...@gmail.com wrote:
> > If they are a class, then you can collect references to all types and
> > execute
> > the comparison at the moemnt of conversion to int.
>
> I do not understand what you mean by this. How do you avoid computing the
> class value using shortcut semantics?
> e.g.
> if(a <=> b && expensiveToCompute() <=> c) {
> }
> how do you avoid running "expensiveToCompute()"? This might even be a
> correctness issue, it could be "crashIfANotEqualToB()"

By delaying the evaluation.

Instead of operator<=> performing the comparison, it returns an object that
has a reference to the left and right sides.

But the ability to generate the other 6 operators is based on `operator<=>` returning a specific set of standard-defined types. You can make it return other things, but the standard won't be able to do the main thing we need `operator<=>` to do.

This also doesn't actually work in the case outlined above. `expensiveToCompute()` calls a function; the return value of that function is what gets applied to `operator<=>`. So by the time it gets stored, it's too late.

Not unless we get the ability to succinctly be able to package a function call+parameters into an expression, such that attempting to access the value of the expression by non-reference will invoke the function.
 

Nicol Bolas

unread,
Dec 4, 2017, 10:45:47 PM12/4/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com
It's not "not proposed"; it's explicitly forbidden. You're not supposed to use `<=>` without a comparison (or just returning it); that's a deliberate design choice. Just as it is a deliberate design choice that the type is not an integer; it is a thing which can be compared against the literal zero. And only the literal zero; you get UB if you try to compare it against anything else.

Alberto Barbati

unread,
Dec 5, 2017, 5:40:36 PM12/5/17
to ISO C++ Standard - Future Proposals


Il giorno martedì 5 dicembre 2017 04:19:35 UTC+1, Thiago Macieira ha scritto:

You should write a paper that provides for a way to create custom operator&&
and operator|| that allow for short-circuiting. That would be useful in other
cases besides operator<=>.


I posted such a proposal on this group some time ago and operator <=> was indeed the first motivating example. I did not have the time recently to work out a few details, but I'd like to repost a new version of it soon.

Alberto

Hyman Rosen

unread,
Dec 5, 2017, 7:30:13 PM12/5/17
to std-pr...@isocpp.org

I've mentioned before that I think the best way to do this is not to do something
special just for those operators, but instead to create a new form of parameter
implementing call-by-name.  That is, when a function has such a parameter, the
compiler does not evaluate the argument for that parameter on a function call.
Instead, it implements a lambda-like construct wrapping the argument and passes
that to the function, and this construct gets invoked when the function evaluates
its argument.

My notional notation is to prefix the parameters with [], so the custom operator&&
would look like this:

struct MyType { bool isTrue() const { return false; } };

bool operator&&([] const MyType &a, [] const MyType &b) {
    return a.isTrue() && b.isTrue();
}

MyType f1() { return MyType(); }
MyType f2() { throw  MyType(); }

int main() { return f1() && f2() ? 0 : 1; }  // returns 1

Ville Voutilainen

unread,
Dec 5, 2017, 7:49:15 PM12/5/17
to ISO C++ Standard - Future Proposals
On 6 December 2017 at 02:29, Hyman Rosen <hyman...@gmail.com> wrote:
> I've mentioned before that I think the best way to do this is not to do
> something
> special just for those operators, but instead to create a new form of
> parameter
> implementing call-by-name. That is, when a function has such a parameter,
> the
> compiler does not evaluate the argument for that parameter on a function
> call.
> Instead, it implements a lambda-like construct wrapping the argument and
> passes
> that to the function, and this construct gets invoked when the function
> evaluates
> its argument.


Which sounds like
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4360.pdf
where the EWG discussion seemed to weakly invite papers on the notion
of "call by lambda",
where the argument is evaluated in the function definition rather than
in its call. Since
that paper was reviewed, it hasn't had a champion, so the idea has petered out.

Dilip Ranganathan

unread,
Dec 5, 2017, 8:44:10 PM12/5/17
to std-pr...@isocpp.org

Todd Fleming

unread,
Dec 5, 2017, 8:49:12 PM12/5/17
to ISO C++ Standard - Future Proposals
On Tuesday, December 5, 2017 at 8:44:10 PM UTC-5, Dilip R wrote:

I just looked through it. Needs more undefined behavior :)
 

m.ce...@gmail.com

unread,
Dec 6, 2017, 7:25:10 AM12/6/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com
I think there is no need for new language feature when you can just write following (assuming tuple get spaceship operator support):

auto operator<=>(const MyType& a, const MyType& b)
{
   auto tied = [] (const MyType& v) { return std::tie(v.some, v.fields, v.to, v.compare); };
   return tied(a) <=> tied(b);
}

the.ultim...@gmail.com

unread,
Dec 6, 2017, 9:02:59 AM12/6/17
to ISO C++ Standard - Future Proposals
I just read the paper. Admittedly hand-wavily, the idea struck me that instead of being something entirely new, it looks a lot as if the called function fn(inline T t) behaved as if it were receiving a std::future<T>&. The called function would then only need to use .get() to control when exactly the right-hand expression calculation would be performed.

Wouldn't there be a track to investigate, to enable operators and functions to take parameters as std::shortcircuitable<T>, and make this type have an API similar to std::future?

Alberto Barbati

unread,
Dec 6, 2017, 1:24:14 PM12/6/17
to ISO C++ Standard - Future Proposals, charle...@gmail.com, m.ce...@gmail.com
You can have your preferences. Speaking for myself, I definitely wouldn't write that, unless under torture. Even without lazy evaluation it is not difficult to write an efficient library function that would allow me to write this:

auto operator<=>(const MyType& a, const MyType& b)
{
   return std::compose_order(
    a.some, b.some,
    a.fields, b.fields,
    a.to, b.to,
    a.compare, b.compare);
}

However, if we had lazy evaluation, I would find this style more compelling:

auto operator<=>(const MyType& a, const MyType& b)
{
   return std::compose_order(
    a.some <=> b.some,
    a.fields <=> b.fields,
    a.to <=> b.to,
    a.compare <=> b.compare);
}

Just my 2 eurocent.
Reply all
Reply to author
Forward
Message has been deleted
Message has been deleted
0 new messages