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