Regarding the precondition of memcmp
yielding the same result as member-wise comparisons with ==
, while this precondition is often fulfilled in practice, it's somewhat brittle.
Changing compilers or compiler options can in theory break that precondition. Of more concern, code maintenance (and 80% of all programming work is maintenance, IIRC) can break it by adding or removing members, making the class polymorphic, adding custom ==
overloads, etc. And as mentioned in one of the comments, the precondition can hold for static variables while it doesn't hold for automatic variables, and then maintenance work that creates non-static objects can do Bad Things™.
And regarding the question of whether to use memcmp
or member-wise ==
to implement an ==
operator for the class, first, this is a false dichotomy, for those are not the only options.
For example, it can be less work and more maintainable to use automatic generation of relational operator overloads, in terms of a compare
function. The std::string::compare
function is an example of such a function.
Secondly, the answer to what implementation to choose depends strongly on what you consider important, e.g.:
should one seek to maximize runtime efficiency, or
should one seek to create clearest code, or
should one seek the most terse, fastest to write code, or
should one seek to make the class most safe to use, or
something else, perhaps?
Generating relational operators.
You may have heard of CRTP, the Curiously Recurring Template Pattern. As I recall it was invented to deal with the requirement of generating relational operator overloads. I may possibly be conflating that with something else, though, but anyway:
template< class Derived >
struct Relops_from_compare
{
friend
auto operator!=( const Derived& a, const Derived& b )
-> bool
{ return compare( a, b ) != 0; }
friend
auto operator<( const Derived& a, const Derived& b )
-> bool
{ return compare( a, b ) < 0; }
friend
auto operator<=( const Derived& a, const Derived& b )
-> bool
{ return compare( a, b ) <= 0; }
friend
auto operator==( const Derived& a, const Derived& b )
-> bool
{ return compare( a, b ) == 0; }
friend
auto operator>=( const Derived& a, const Derived& b )
-> bool
{ return compare( a, b ) >= 0; }
friend
auto operator>( const Derived& a, const Derived& b )
-> bool
{ return compare( a, b ) > 0; }
};
Given the above support, we can investigate the options available for your question.
Implementation A: comparison by subtraction.
This is a class providing a full set of relational operators without using either memcmp
or ==
:
struct Vector
: Relops_from_compare< Vector >
{
int x, y, z;
// This implementation assumes no overflow occurs.
friend
auto compare( const Vector& a, const Vector& b )
-> int
{
if( const auto r = a.x - b.x ) { return r; }
if( const auto r = a.y - b.y ) { return r; }
return a.z - b.z;
}
Vector( const int _x, const int _y, const int _z )
: x( _x ), y( _y ), z( _z )
{}
};
Implementation B: comparison via memcmp
.
This is the same class implemented using memcmp
; I think you'll agree that this code scales better and is simpler:
struct Vector
: Relops_from_compare< Vector >
{
int x, y, z;
// This implementation requires that there is no padding.
// Also, it doesn't deal with negative numbers for < or >.
friend
auto compare( const Vector& a, const Vector& b )
-> int
{
static_assert( sizeof( Vector ) == 3*sizeof( x ), "!" );
return memcmp( &a, &b, sizeof( Vector ) );
}
Vector( const int _x, const int _y, const int _z )
: x( _x ), y( _y ), z( _z )
{}
};
Implementation C: comparison member by member.
This is an implementation using member-wise comparisons. It doesn't impose any special requirements or assumptions. But it's more source code.
struct Vector
: Relops_from_compare< Vector >
{
int x, y, z;
friend
auto compare( const Vector& a, const Vector& b )
-> int
{
if( a.x < b.x ) { return -1; }
if( a.x > b.x ) { return +1; }
if( a.y < b.y ) { return -1; }
if( a.y > b.y ) { return +1; }
if( a.z < b.z ) { return -1; }
if( a.z > b.z ) { return +1; }
return 0;
}
Vector( const int _x, const int _y, const int _z )
: x( _x ), y( _y ), z( _z )
{}
};
Implementation D: compare
in terms of relational operators.
This is an implementation sort of reversing the natural order of things, by implementing compare
in terms of <
and ==
, which are provided directly and implemented in terms of std::tuple
comparisons (using std::tie
).
struct Vector
{
int x, y, z;
friend
auto operator<( const Vector& a, const Vector& b )
-> bool
{
using std::tie;
return tie( a.x, a.y, a.z ) < tie( b.x, b.y, b.z );
}
friend
auto operator==( const Vector& a, const Vector& b )
-> bool
{
using std::tie;
return tie( a.x, a.y, a.z ) == tie( b.x, b.y, b.z );
}
friend
auto compare( const Vector& a, const Vector& b )
-> int
{
return (a < b? -1 : a == b? 0 : +1);
}
Vector( const int _x, const int _y, const int _z )
: x( _x ), y( _y ), z( _z )
{}
};
As given, client code using e.g. >
needs a using namespace std::rel_ops;
.
Alternatives include adding all other operators to the above (much more code), or using a CRTP operator generation scheme that implements the other operators in terms of <
and =
(possibly inefficiently).
Implementation E: comparision by manual use of <
and ==
.
This implementation is the result not applying any abstraction, just banging away at the keyboard and writing directly what the machine should do:
struct Vector
{
int x, y, z;
friend
auto operator<( const Vector& a, const Vector& b )
-> bool
{
return (
a.x < b.x ||
a.x == b.x && (
a.y < b.y ||
a.y == b.y && (
a.z < b.z
)
)
);
}
friend
auto operator==( const Vector& a, const Vector& b )
-> bool
{
return
a.x == b.x &&
a.y == b.y &&
a.z == b.z;
}
friend
auto compare( const Vector& a, const Vector& b )
-> int
{
return (a < b? -1 : a == b? 0 : +1);
}
Vector( const int _x, const int _y, const int _z )
: x( _x ), y( _y ), z( _z )
{}
};
What to choose.
Considering the list of possible aspects to value most, like safety, clarity, efficiency, shortness, evaluate each approach above.
Then choose the one that to you is clearly best, or one of the approaches that seem about equally best.
Guidance: For safety you would not want to choose approach A, subtraction, since it relies on an assumption about the values. Note that also option B, memcmp
, is unsafe as an implementation for the general case, but can do well for just ==
and !=
. For efficiency you should better MEASURE, with relevant compiler options and environment, and remember Donald Knuth's adage: “premature optimization is the root of all evil” (i.e. spending time on that may be counter-productive).
operator==
which calls==
on all its members automatically. But they don't. :( – Frielz
to the struct, you're supposed to actively decide whether or notz
participates in the value for equality purposes, and modifyoperator==
or not accordingly, along with comparisons and hashcodes. So for example invector
the capacity doesn't participate in equality comparison (and of course there are plenty of other reasons whymemcmp
isn't even close to working forvector
!). Yes, it would be nice to be able to= default
it, but even if yourT
has this property, it won't necessarily still have the property after addingz
. – Monopode==
to amemcmp
if he knows there is no padding. So performance-wise they are going to be the same, but operator== is portable. – Phippsmemcmp
instead of producing a loop and comparing each byte, if possible. – Churchlikememcmp
is a lexicographical compare, no? Therefore, it is ill-suited to compare structs containing types likeint
andlong
, which can be compared word by word instead of byte by byte. – Gullettmemcmp
as a call to a compare-memory method, better compilers regard it as an intrinsic, and will only call a general-purpose library method in cases where nothing better is available. Givenint32_t i,j; float f,g;
, a good compiler may turnif (memcmp(&i,&j,4))
intoif (i!=j)
, andif (memcmp(&f,&g,4))
intoif (*(int32_t*)&f != *(int32_t*)&g)
. – Gatefold