Thread overview
implementing default opCmp
Nov 18, 2020
Paul Backus
Nov 18, 2020
ag0aep6g
Nov 19, 2020
Ali Çehreli
November 18, 2020
I have a struct like this:

struct S
{
   int x;
   int y;
}

and I want a default comparison. The problem is, that comparison doesn't have a default, and requires I implement opCmp. While this is useful for the compiler, there's no default I know of that is an easy one-liner.

The truth is, I'm not entirely caring what order these things come out in. I just want them to be defined as having an order given that all the members have a defined order. My expectation is that a default opCmp would look like:

int opCmp(S other)
{
   if(x == other.x)
   {
       if(y == other.y) return 0;
       return y < other.y ? -1 : 1;
   }
   return x < other.x ? -1 : 1;
}

But really, as long as there is something to do this easily I don't care what the ordering turns out to be.

I can do equality like:

return this.tupleof == other.tupleof;

I can do assignment like:

this.tupleof = other.tupleof;

How do I do something really simple for opCmp? I tried this it didn't work:

return this == other ? 0 :
    this.tupleof < other.tupleof ? -1 : 1;

-Steve
November 18, 2020
On Wednesday, 18 November 2020 at 22:29:17 UTC, Steven Schveighoffer wrote:
> I have a struct like this:
>
> struct S
> {
>    int x;
>    int y;
> }
>
> and I want a default comparison. The problem is, that comparison doesn't have a default, and requires I implement opCmp. While this is useful for the compiler, there's no default I know of that is an easy one-liner.

Here's a stab at a totally generic version that I haven't unit tested at all, except to verify that it works for your example struct S:

auto cmp(T, U)(auto ref T lhs, auto ref U rhs)
{
    import core.lifetime: forward;

    static if (__traits(compiles, lhs.opCmp(rhs)))
        return forward!lhs.opCmp(forward!rhs);
    else static if (__traits(compiles, rhs.opCmp(lhs)))
        return -forward!rhs.opCmp(forward!lhs);
    else
        return lhs < rhs ? -1 : lhs > rhs ? 1 : 0;
}

mixin template defaultOpCmp()
{
    import std.traits: isAggregateType;

    static assert(isAggregateType!(typeof(this)),
    	"opCmp can only be overloaded for aggregate types.");

    auto opCmp()(auto ref typeof(this) other)
    {
        import std.traits: ReturnType, CommonType, Fields;
        import std.meta: Map = staticMap;

        alias cmpType(T) = ReturnType!((T lhs, T rhs) => cmp(lhs, rhs));
        alias Result = CommonType!(Map!(cmpType, Fields!(typeof(this))));

        Result result;

        static foreach (i, _; typeof(this).tupleof)
            if (result == 0)
            	result = cmp(this.tupleof[i], other.tupleof[i]);

        return result;
    }
}
November 18, 2020
On Wednesday, 18 November 2020 at 22:29:17 UTC, Steven Schveighoffer wrote:
> How do I do something really simple for opCmp? I tried this it didn't work:
>
> return this == other ? 0 :
>     this.tupleof < other.tupleof ? -1 : 1;

std.typecons.Tuple has opCmp. So this works:

    int opCmp(S other)
    {
        import std.typecons: tuple;
        return tuple(this.tupleof).opCmp(tuple(other.tupleof));
    }
November 19, 2020
On 11/18/20 6:02 PM, Paul Backus wrote:
> On Wednesday, 18 November 2020 at 22:29:17 UTC, Steven Schveighoffer wrote:
>> I have a struct like this:
>>
>> struct S
>> {
>>    int x;
>>    int y;
>> }
>>
>> and I want a default comparison. The problem is, that comparison doesn't have a default, and requires I implement opCmp. While this is useful for the compiler, there's no default I know of that is an easy one-liner.
> 
> Here's a stab at a totally generic version that I haven't unit tested at all, except to verify that it works for your example struct S:
> 
> auto cmp(T, U)(auto ref T lhs, auto ref U rhs)
> {
>      import core.lifetime: forward;
> 
>      static if (__traits(compiles, lhs.opCmp(rhs)))
>          return forward!lhs.opCmp(forward!rhs);
>      else static if (__traits(compiles, rhs.opCmp(lhs)))
>          return -forward!rhs.opCmp(forward!lhs);
>      else
>          return lhs < rhs ? -1 : lhs > rhs ? 1 : 0;
> }
> 
> mixin template defaultOpCmp()
> {
>      import std.traits: isAggregateType;
> 
>      static assert(isAggregateType!(typeof(this)),
>          "opCmp can only be overloaded for aggregate types.");
> 
>      auto opCmp()(auto ref typeof(this) other)
>      {
>          import std.traits: ReturnType, CommonType, Fields;
>          import std.meta: Map = staticMap;
> 
>          alias cmpType(T) = ReturnType!((T lhs, T rhs) => cmp(lhs, rhs));
>          alias Result = CommonType!(Map!(cmpType, Fields!(typeof(this))));
> 
>          Result result;
> 
>          static foreach (i, _; typeof(this).tupleof)
>              if (result == 0)
>                  result = cmp(this.tupleof[i], other.tupleof[i]);
> 
>          return result;
>      }
> }

Yeah, something like this might be useful in druntime. But it makes you wonder if we wouldn't be better off without opCmp but instead with opBinary(string s : "<") and friends.

One thing that sucks is that opCmp might do more operations than are necessary for the actual comparison, because it has to generate the numeric result.

-Steve
November 19, 2020
On 11/18/20 6:06 PM, ag0aep6g wrote:
> On Wednesday, 18 November 2020 at 22:29:17 UTC, Steven Schveighoffer wrote:
>> How do I do something really simple for opCmp? I tried this it didn't work:
>>
>> return this == other ? 0 :
>>     this.tupleof < other.tupleof ? -1 : 1;
> 
> std.typecons.Tuple has opCmp. So this works:
> 
>      int opCmp(S other)
>      {
>          import std.typecons: tuple;
>          return tuple(this.tupleof).opCmp(tuple(other.tupleof));
>      }


Ah, excellent solution! I hadn't thought of that.

-Steve
November 19, 2020
On 11/19/20 6:12 AM, Steven Schveighoffer wrote:
> On 11/18/20 6:06 PM, ag0aep6g wrote:

>>      int opCmp(S other)
>>      {
>>          import std.typecons: tuple;
>>          return tuple(this.tupleof).opCmp(tuple(other.tupleof));
>>      }
> 
> 
> Ah, excellent solution! I hadn't thought of that.
> 
> -Steve

That's what I use as well.

S can be replaced with something like 'typeof(this)' (or perhaps 'ref const(typeof(this))' and throw some inout in there :) ) and the whole thing can be mixed-in whereever needed.

Ali