Can a "future-safe" compare operator be crafted in C++?
Greetings all,
Is it possible to craft a future-safe comparison operator (==) in C++?
The problem I faced was that we have a class with multiple members. We have a comparison operator to validate if instance-1 of the object has the same values as instance-2.
i.e. we can do
class blarg {
.....
};
.....
blarg b1(..initializers...);
blarg b2 = b1;
if (b1 == b2) {
... then do something ....
}
However, I had a co-worker that added a new member to the class, but failed to update the comparison operator. This lead to problems that took a while for us to figure out.
Is there a coding practice, I mean other than code review (that failed for us), or coding method, desi开发者_如何学JAVAgn, template, magic beans, whatever that could help avoid situations like this?
My first reaction was to use the memcmp
command. However, after reading the stack overflow entry for "Comparing structures in C vs C++" I see that this can be problematic due to C++ classes having more than just the member data inside.
How do others deal with this?
Thank you in advance for your help.
Well, the obvious solution is to be more careful when you extend the original class. :) "Being more careful" includes things like code reviews, things like that, but obviously this isn't fool proof.
So tackling this problem from a philosophical point of view, rather than a technical one, can often provide insight. The philosophy in this case is that of being a paranoid programmer. Assume that code you write today will be broken by some nitwit months or years from now. (Edit per @Noah's comments below: More often than not, that nitwit is myself. Being a paranoid programmer protects me from myself probably more than anyone else.) If you can do something to ensure that when the nitwit does break your code something fails before the product is shipped, that would help.
Two things I like to use are static assertions and unit tests. A static assertion can be used in your operator==
code to verify that the sizeof
your class is what you expect it to be. For example:
bool MyClass::operator==(const MyClass& rhs) const
{
static_assert(sizeof(MyClass) == sizeof(foo_) + sizeof(bar_))
...
}
...where foo_
and bar_
are member variables. This will break the compile when the size of the class has changed.
As static assertion usually takes the form of a template class that will fail to compile when the expression is false. This can be a little tricky to write (but it's an interesting exercise -- consider what happens if you try to add a char test_[0]
member to a class). Fortunately, this wheel has already been invented. See Boost for an example, and I think the new MSVC compiler comes with one as well.
OK, real answer now.
A proper unit test for your object totally should have turned this kind of error up. It's exactly the kind of thing they're good at pointing out.
The only real way to solve this is to not need to have an assignment operator for that class at all. What's that, you ask? What if I have dynamic memory, etc., that requires a deep copy?
If someone is often adding data members to your class, that means the class is not a simple container for that data. By wrapping up whatever it is that needs the custom assignment operator into a separate, simple class, you can put the custom assignment code into that simple class, replace the data in the complex class that requires custom assignment with an instance of that new simple class, and avoid needing to write a custom assignment operator for the complex class.
The simple class probably won't change often, if ever, so there will be less assignment-operator maintenance to do. Odds are, going through this process will also end up leaving you with a cleaner and more workable architecture.
In some cases, it can be done by making all members some form of property or similar that can be iterated through and compared individually at runtime.
For normal classes this is not really an option though - I would recommend having unit tests for this, like Noah Roberts pointed out in his answer.
May sound silly, but you could write a list of all the things that (may) need to account for all members of a class. I can think of:
- user-defined constructors (in particular, their initializer lists)
- destructor (if your class ever uses members that need it, although if the destructor needs to account for more than one member you're probably in trouble)
swap
operator<<(std::ostream&, const blarg&)
, or other forms of (de)serializationoperator<
operator==
- [C++0x]
hash
function - probably something else I've forgotten.
Run through the checklist when you add a member, or change the type or meaning of a member (or possibly remove one, but in that case anything using it generally explodes of its own accord, and anything not using it probably won't notice it's gone).
It's easier if all such functions are defined (or at least declared) together: certainly I keep the first three "lifecycle" things together anyway, and I never touch a member without reviewing them. The three "comparison" ones ideally are close together too, since they need to be mutually consistent, so once the habit is ingrained, the checklist might end up being quite a simple glance over the class. "I've changed the class stucture, so I need to look here to see what functions to review in addition to the changes I already have in mind..."
Obviously if you can manage it, you follow the Open/Closed principle, and this never happens. Personally I've never managed that.
精彩评论