| |
| Posted by Ali Çehreli | PermalinkReply |
|
Ali Çehreli
| 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
|