Section 13:
|
[13.9] What are some guidelines / "rules of thumb" for overloading operators?
Here are a few guidelines / rules of thumb (but be sure to read
the previous FAQ before reading this
list):
- Use common sense. If your overloaded operator makes life easier and safer
for your users, do it; otherwise don't. This is the most important guideline.
In fact it is, in a very real sense, the only guideline; the rest are just
special cases.
- If you define arithmetic operators, maintain the usual arithmetic
identities. For example, if your class defines x + y and x -
y, then x + y - y ought to return an object that is behaviorally
equivalent to x. The term behaviorally equivalent is defined in the
bullet on x == y below, but simply put, it means the two objects
should ideally act like they have the same state. This should be true even if
you decide not to define an == operator for objects of your class.
- You should provide arithmetic operators only when they make logical sense
to users. Subtracting two dates makes sense, logically returning the duration
between those dates, so you might want to allow date1 - date2 for
objects of your Date class (provided you have a reasonable class/type
to represent the duration between two Date objects). However adding
two dates makes no sense: what does it mean to add July 4, 1776 to June 5,
1959? Similarly it makes no sense to multiply or divide dates, so you should
not define any of those operators.
- You should provide mixed-mode arithmetic operators only when they make
logical sense to users. For example, it makes sense to add a duration (say 35
days) to a date (say July 4, 1776), so you might define date +
duration to return a Date. Similarly date - duration
could also return a Date. But duration - date does not make
sense at the conceptual level (what does it mean to subtract July 4, 1776 from
35 days?) so you should not define that operator.
- If you provide constructive operators, they should return their result by
value. For example, x + y should return its result by value. If it
returns by reference, you will probably run into lots of problems figuring out
who owns the referent and when the referent will get destructed. Doesn't
matter if returning by reference is more efficient; it is probably
wrong. See the next bullet for more on this point.
- If you provide constructive operators, they should not change their
operands. For example, x + y should not change x. For some
crazy reason, programmers often define x + y to be logically the same
as x += y because the latter is faster. But remember, your users
expect x + y to make a copy. In fact they selected the
+ operator (over, say, the += operator) precisely because they
wanted a copy. If they wanted to modify x, they would have
used whatever is equivalent to x += y instead. Don't make semantic
decisions for your users; it's their decision, not yours, whether they
want the semantics of x + y vs. x += y. Tell them that one is
faster if you want, but then step back and let them make the final decision
— they know what they're trying to achieve and you do not.
- If you provide constructive operators, they should allow promotion of the
left-hand operand (at least in the case where the class has a single-parameter
ctor that is not marked with the
explicit keyword). For example, if your class Fraction
supports promotion from int to Fraction (via the
non-explicit ctor Fraction::Fraction(int)), and if you allow
x - y for two Fraction objects, you should also allow 42 -
y. In practice that simply means that your operator-() should not
be a member function of Fraction. Typically you will make it a
friend, if for no other reason than to
force it into the public: part of
the class, but even if it is not a friend, it should not be a
member.
- In general, your operator should change its operand(s) if and only if the
operands get changed when you apply the same operator to intrinsic types.
x == y and x << y should not change either operand; x *=
y and x <<= y should (but only the left-hand operand).
- If you define x++ and ++x, maintain the usual identities.
For example, x++ and ++x should have the same
observable effect on x, and should differ only in what they return.
++x should return x by reference; x++ should either
return a copy (by value) of the original state of x or should have a
void return-type. You're usually better off returning a copy of the
original state of x by value, especially if your class will be used in
generic algorithms. The easy way to do that is to implement x++ using
three lines: make a local copy of *this, call ++x (i.e.,
this->operator++()), then return the local copy. Similar comments for
x-- and --x.
- If you define ++x and x += 1, maintain the usual
identities. For example, these expressions should have the same observable
behavior, including the same result. Among other things, that means your
+= operator should return x by reference. Similar comments
for --x and x -= 1.
- If you define *p and p[0] for pointer-like objects,
maintain the usual identities. For example, these two expressions should have
the same result and neither should change p.
- If you define p[i] and *(p+i) for pointer-like objects,
maintain the usual identities. For example, these two expressions should have
the same result and neither should change p. Similar comments for
p[-i] and *(p-i).
- Subscript operators generally come in pairs; see on
const-overloading.
- If you define x == y, then x == y should be true if and
only if the two objects are behaviorally equivalent. In this bullet, the term
"behaviorally equivalent" means the observable behavior of any operation or
sequence of operations applied to x will be the same as when applied
to y. The term "operation" means methods, friends, operators, or just
about anything else you can do with these objects (except, of course, the
address-of operator). You won't always be able to achieve that goal, but you
ought to get close, and you ought to document any variances (other than the
address-of operator).
- If you define x == y and x = y, maintain the usual
identities. For example, after an assignment, the two objects should be
equal. Even if you don't define x == y, the two objects should be
behaviorally equivalent (see above for the meaning of that phrase) after an
assignment.
- If you define x == y and x != y, you should maintain the
usual identities. For example, these expressions should return something
convertible to bool, neither should change its operands, and x ==
y should have the same result as !(x != y), and vice versa.
- If you define inequality operators like x <= y and x < y,
you should maintain the usual identities. For example, if x < y and
y < z are both true, then x < z should also be true, etc.
Similar comments for x >= y and x > y.
- If you define inequality operators like x < y and x >= y,
you should maintain the usual identities. For example, x < y should
have the result as !(x >= y). You can't always do that, but you
should get close and you should document any variances. Similar comments for
x > y and !(x <= y), etc.
- Avoid overloading short-circuiting operators: x || y or x &&
y. The overloaded versions of these do not short-circuit — they
evaluate both operands even if the left-hand operand "determines" the outcome,
so that confuses users.
- Avoid overloading the comma operator: x, y. The overloaded comma
operator does not have the same ordering properties that it has when it is not
overloaded, and that confuses users.
- Don't overload an operator that is non-intuitive to your users. This is
called the Doctrine of Least Surprise. For example, although C++ uses
std::cout << x for printing, and although printing is technically
called inserting, and although inserting sort of sounds like what happens when
you push an element onto a stack, don't overload myStack << x to push
an element onto a stack. It might make sense when you're really tired or
otherwise mentally impaired, and a few of your friends might think it's
"kewl," but just say No.
- Use common sense. If you don't see "your" operator listed here, you can
figure it out. Just remember the ultimate goals of operator overloading: to
make life easier for your users, in particular to make their code cheaper to
write and more obvious.
Caveat: the list is not exhaustive. That means there are other entries that
you might consider "missing." I know.
Caveat: the list contains guidelines, not hard and fast rules. That means
almost all of the entries have exceptions, and most of those exceptions are
not explicitly stated. I know.
Caveat: please don't email me about the additions or exceptions. I've already
spent way too much time on this particular answer.
|