[...]
What you've said about the arbitrary semantics is certainly
true. It is common to define operator< to be as fast as
possible, while distinguishing objects that are not == in a
consistent, so that the objects can be stored in a std::set.
It's even more common to not define operator< at all, since it's
very common to have no natural ordering over the type, and most
applications still use hash tables, rather than std::set. Even
in places where std::set has replaced older hash tables, it's
usual to only implement operator< on an "as needed" basis. Or
even more often, provide a special comparison operator to be
used, rather than operator<, since an operator< which doesn't
implement a true less than relationship is confusing to the
users.
(Of course, comparison according to specific criteria are also
common, e.g. comparing (x,y) points in y-major order.)
That's a different case, but it doesn't really apply much to the
sort of things I work on. In fact, in many cases, it's almost
the opposite: there's no real logical ordering, but because the
identity is represented as an integral type (e.g. serial
number, etc.), we get an operator< which is (logically speaking)
arbitrary for free.
I don't think I've ever seen operator== defined *before*
operator<, and if I did, I would hope that the idea of
replacing its (often non-trivial) definition with a one-liner
like !(a < b || b < a) would at least be considered.
That is, quite frankly, crazy. First and foremost, you write
code to be read; that's *not* the intuitive definition of
equality in most peoples minds, and qualifies as obfuscation.
Except in exceptional cases, such an implementation of
operator== wouldn't get past code review.
If there is a compelling performance reason, it can make sense
to have a separate operator==. That does not seem to be the
common case, however.
Well, readability is of course the first consideration.
Comparing lengths improves readability by allowing a simpler
loop (or direct use of std::equals, today). In this case,
readability works with performance: a simpler loop is not only
easier to read, it will be faster (since I'll have one less
condition to test each time through).
Taking your example: Do you actually know that it is faster
to check lengths first, or have you only assumed it?
To tell the truth, I just picked an example out of my hat. The
code *is* easier to read and to understand if you check the
lengths first---a loop comparing two sequences of equal length
is simpler than one comparing sequences of possibly unknown
length.
Supposed that strings commonly begin with any of thirty
different characters (including case distinctions), but have
only ten common lengths. A traditional, lexicographical
comparison may be faster without the a priori "optimization"
of checking lengths. (Of course, operator== might still be
faster than two calls to operator<, but probably not
compellingly so.)
But implementing operator== in terms of operator< would still be
obfuscation.
If such a comparison function already exists, then of course
it makes sense to use it, but I have not found this to be the
most common situation. (It was once upon a time; I vaguely
remember defining operators in terms of strcmp...)
Most places where I've worked, coding guidelines have imposed
it. In a lot of cases, the operators didn't really get
implemented until later, long after isEqual(). And of course,
compare() has been a standard idiom for ages, for a number of
reasons. (Note that it is still present in std::string.)
We also don't always get to define the types we're comparing.
Nope. That's why all of the functions and containers which
depend on ordering have forms where you can supply an arbitrary
comparison function. In cases where I expect a type to be used
as an index, but there is no logical ordering, I'll normally
provide an additional type which establishes the arbitrary
ordering, rather that operator<.
I have recently been working with a library that provides a
lot of useful data types, but does not provide comparators for
them. Users of the library frequently define special-purpose
comparison functions for use with std::sort, std::set, etc.
Boost is hardly an example of what "typical" programmers do.
It's interesting that you hope programmers "typically" use
Barton/Nackman, but you don't think it would occur to them to
define comparison operators in terms of each other.
It's not "hope"---it's what I've actually seen in practice.
The Barton and Nackman trick has been around a lot longer than
the STL. And using it allows writing code in a more natural
fashion.
I seem to vaguely recall an older version of GCC (2.95?)
generating operators automatically, but newer versions don't
seem to do it. Maybe it was some other library I was using at
the time...
I don't remember this (and I've been using g++ off and on since
1.49), although it may have been present without my realizing
it. I rather doubt it was as late as 2.95, however---by that
time (and long before that, in fact), standards conformance had
become very important to the g++ developers.