Also, the operator overloading page is obscure when it comes to
opEquals and opCmp... specifically, it doesn't say what a class's
opCmp should return under what conditions.

Indeed, it expects you to be able to work it out from what it does
tell you about them.
Specifically:
a.opEquals(b) != 0 for a == b
a.opEquals(b) == 0 for a != b
a.opCmp(b) < 0 for a < b
a.opCmp(b) > 0 for a > b
a.opcmp(b) == 0 for !(a < b || a > b)
Stewart.

Indeed, it expects you to be able to work it out from what it does
tell you about them.
Specifically:
a.opEquals(b) != 0 for a == b
a.opEquals(b) == 0 for a != b
a.opCmp(b) < 0 for a < b
a.opCmp(b) > 0 for a > b
a.opcmp(b) == 0 for !(a < b || a > b)
Stewart.

Hm. That last option answers something I was greatly concerned about. Namely,
how should I handle a(opCmp(NaN))...

How does D handle comparisons between floating point primitives where one is
NaN?

It doesn't use opCmp(). opCmp() is for objects, not primatives (although one can
use it to compare an object /with/ a primitive, if such a comparison makes
sense).

I would mimic this behavior.

It is not possible to mimic the behavior of float or complex primatives in an
object, because opCmp() just is not powerful enough (it doesn't handle the cases
<>= or !<>=).
I remember suggesting a while back that opCmp() should be modified to return an
enum { LESS, EQUAL, GREATER, INCOMPARABLE }, but few people liked the idea, and
it sort of got lost in another "why does Object define opCmp() anyway?" thread.
Arcane Jill

Indeed, it expects you to be able to work it out from what it does
tell you about them.
Specifically:
a.opEquals(b) != 0 for a == b
a.opEquals(b) == 0 for a != b
a.opCmp(b) < 0 for a < b
a.opCmp(b) > 0 for a > b
a.opcmp(b) == 0 for !(a < b || a > b)
Stewart.

Hm. That last option answers something I was greatly concerned about.

how should I handle a(opCmp(NaN))...

The table of floating point comparison operators in
http://www.digitalmars.com/d/expression.html#RelExpression
says for a floating point variable "a" evaluating "a < NaN" throws an
exception. So based on that a.opCmp(NaN) should throw an exception
(depending on just what your class is doing of course). I don't see a way of
overloading the "unordered" operators like !<>= but presumably that will
come some day. I don't even know what !<>= does with objects or objects that
overload opCmp and/or opEquals. Maybe it just assumes anything overloading
opCmp always has a total order.

I compiled this (wrapped in a main()) and for me it actually outputs 2
(as I expected)

Hm. For some reason, a test program I wrote on my own also showed the expected
behavior. But for my much larger BigDecimal library script, it's unmistakably
doing something unexpected.
I'll try to reduce all the fluff into a minimum testcase to see what I did
wrong. Thanks everyone, though.

class Foo {
private bit[] bool = [0];
unittest {
Foo a = new Foo;
Foo b = new Foo;
a.bool[0] = 1;
assert(b.bool[0] == 0);
}
}
int main() {
return 0;
}
One is left to wonder if that was a bug on the part of the programmer or not.

class Foo {
private bit[] bool = [0];
unittest {
Foo a = new Foo;
Foo b = new Foo;
a.bool[0] = 1;
assert(b.bool[0] == 0);
}
}
int main() {
return 0;
}
One is left to wonder if that was a bug on the part of the programmer or

This should be the same issue as for string literals. The field "bool"
(confusing name, by the way, given that bool is defined in object.d as an
alias for bit) is initialized to have length 1 and data pointer pointing to
a shared (read-only on Linux) bit containing 0. To obtain distinct copies
one needs to have a constructor that allocates the array:
class Foo {
private bit[] bool;
this() { bool = new bit[1]; }
unittest {
Foo a = new Foo;
Foo b = new Foo;
a.bool[0] = 1;
assert(b.bool[0] == 0);
}
}
int main() {
return 0;
}