Lexicographical object comparison by selected members of a struct

Ali Çehreli acehreli at yahoo.com
Sat Aug 21 04:34:46 UTC 2021


Sometimes I need comparison operators that should consider only some 
members of a struct:

struct S {
   int year;         // Primary member
   int month;        // Secondary member

   string[] values;  // Irrelevant
}

I've been using the laziest tuple+tupleof solution in some of my structs:

   bool opEquals(const typeof(this) that) {
     import std.typecons : tuple;

     return tuple(this.tupleof).opEquals(tuple(that.tupleof));
   }

   // Similar for opCmp.

That is repetitive, expensive at run time, and does not satisfy a 
requirement: Some members (like 'values' above) should not be considered 
during comparison.

I am sure you must have come up with similar solutions like the 
following code, or perhaps this whole thing exists in Phobos but I just 
wrote it today... for fun... :) (I think it exists somewhere but I could 
not find it.)  The code is not used in production yet but it should 
allow me to do the following:

struct S {
   int year;         // Primary member
   int month;        // Secondary member

   string[] values;  // Irrelevant

   mixin MemberwiseComparison!(year, month);  // 'values' excluded; good
}

What do you think?

I have a feeling that e.g. extracting member names from the strings like 
"this.foo" with the help of findSplitAfter(members[i].stringof) is 
pretty suspect. Could I do better?

At least the generated assembly is minimal to my eyes. (Much better than 
my lazy tuple+tupleof complication! :) )

// This helper function is defined outside of MemberwiseComparison
// because we don't want to mixin such a member function into user
// structs.
private string memberName(string thisName) {
   import std.algorithm : findSplitAfter;
   import std.exception : enforce;
   import std.range : empty;
   import std.format : format;

   const found = thisName.findSplitAfter(".");
   enforce(!found[0].empty,
           format!`Failed to find '.' in "%s"`(thisName));

   return found[1];
}

unittest {
   assert(memberName("this.foo") == "foo");

   // It should throw when no '.' is found.
   import std.exception;
   assertThrown(memberName("woo/hoo"));
}

// Mixes in opEquals() and opCmp() that perform lexicographical
// comparisons according to the order of 'members'.
mixin template MemberwiseComparison(members...) {
   bool opEquals(const typeof(this) that) const {

     // A nested function that makes a comparison expression.
     string makeCode(string name) {
       import std.format : format;

       return format!q{
         const a = this.%s;
         const b = that.%s;

         if (a != b) {
           // Early return at first mismatch
           return false;
         }
       }(name, name);
     }

     // Comparison code per member, which would potentially return an
     // early 'false' result.
     static foreach (i; 0 .. members.length) {{
       mixin (makeCode(memberName(members[i].stringof)));
     }}

     // There was no mismatch if we got here.
     return true;
   }

   int opCmp(const typeof(this) that) const {

     // A nested function that makes a comparison expression.
     string makeCode(string name) {
       import std.format : format;

       return format!q{
         const a = this.%s;
         const b = that.%s;

         if (a < b) {
           // 'this' is before; early return
           return -1;
         }

         if (a > b) {
           // 'this' is after; early return
           return  1;
         }
       }(name, name);
     }

     // Comparison code per member, which would potentially return an
     // early before or after decision.
     static foreach (i; 0 .. members.length) {{
       mixin (makeCode(memberName(members[i].stringof)));
     }}

     // Neither 'this' or 'that' was before if we got here.
     return 0;
   }
}

unittest {
   struct S {
     int i;
     double d;
     string s;

     // Only i and d are considered for this type.
     mixin MemberwiseComparison!(i, d);
   }

   // The order is decided by the first member.
   assert(S(1, 2) < S(2, 2));
   assert(S(3, 2) > S(2, 2));

   // The order is decided by the second member.
   assert(S(1, 2) < S(1, 3));
   assert(S(1, 2) > S(1, 1));

   // Objects are equal regardless of the third member.
   assert(S(7, 42, "hello") == S(7, 42, "world"));
}

void main() {
}

Ali


More information about the Digitalmars-d-learn mailing list