The obvious examples of appropriate operator overloading are any classes which behave in the same way that numbers operate. So BigInt classes (as Jalayn suggests), complex numbers or matrix classes (as Superbest suggests) all have the same operations that ordinary numbers have so map really well onto mathematical operators, while time operations (as suggested by svick) map nicely onto a subset of those operations.
Slightly more abstractly, operators could be used when performing set like operations, so
operator+ could be a union,
operator- could be a complement etc. This does start to stretch the paradigm though, especially if you use the addition or multiply operator for an operation which isn't commutative, as you might expect them to be.
C# itself has an excellent example of non-numeric operator overloading. It uses
-= to add and subtract delegates, i.e. register and de-register them. This works well because the
-= operators work as you would expect them to, and this result in much more concise code.
For the purist, one of the problems with the string
+ operator is that it isn't commutative.
"a"+"b" is not the same as
"b"+"a". We understand this exception for strings because it is so common, but how can we tell if using
operator+ on other types will be commutative or not? Most people will assume that it is, unless the object is string-like, but you never really know what people will assume.
As with strings, the foibles of matrices are pretty well known too. It is obvious that
Matrix operator* (double, Matrix) is a scalar multiplication, whereas
Matrix operator* (Matrix, Matrix) would be a matrix multiplication (i.e. a matrix of dot-product multiplications) for instance.
Similarly the use of operators with delegates is so obviously far removed from maths that you are unlikely to make those mistakes.
Incidentally, at the 2011 ACCU conference, Roger Orr & Steve Love presented a session on Some objects are more equal than others - a look at the many meanings of equality, value and identity. Their slides are downloadable, as is Richard Harris' Appendix about floating point equality. Summary: Be very careful with
operator==, here be dragons!
Operator overloading is a very powerful semantic technique, but it is easy to over-use. Ideally you should only use it in situations when it is very clear from context what the effect of an overloaded operator is. In many ways
a.union(b) is clearer than
a*b is much more obscure than
a.cartesianProduct(b), especially since the result of a cartesian product would be a
SetLike<Tuple<T,T>> rather than a
The real problems with operator overloading come when a programmer assumes a class will behave in one way, but it actually behaves in another. This sort of semantic clash is what I'm suggesting it is important to try to avoid.