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

Reply via email to