10

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, design, 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.

Community
  • 1
  • 1
John Rocha
  • 1,656
  • 3
  • 19
  • 30

5 Answers5

8

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.

John Dibling
  • 99,718
  • 31
  • 186
  • 324
7

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.

Edward Strange
  • 40,307
  • 7
  • 73
  • 125
  • 4
    As long as the programmer who added the member variable to the class also adds the proper check in the unit tests for the same change. He failed to update `operator==`, so what is the likelihood he will remember to update his unit tests? – Zac Howland Dec 22 '10 at 17:43
  • Even this won't help. If the developer who added the new member forgot about the comparison operator, he might just as well also forget to write a unit test. – Daniel Lidström Dec 22 '10 at 17:44
  • 2
    Two responses to that one. First off, correct use of unit testing dictates that you write the test FIRST. The developer should not forget if they follow that approach. Second, if the test in question HAS a test for ==, as it well should if it has such an operator, then failure to update it should still result in failure to compile and remind the developer of his mistake. But no, still not perfect even though it greatly decreases the odds of screwup. – Edward Strange Dec 22 '10 at 17:49
  • @Daniel, not really. It's easier to forget to update a comparison operator than it is to forget to do a unit test. The first is an understandable oversight, but the second is plain negligence. – Charles Salvia Dec 22 '10 at 17:52
  • @Noah: You are right. CORRECT use of unit testing does dictate that. However, it is quite apparent that this developer did NOT do so. You can only account for human error so much before the human has to learn to think for themselves. – Zac Howland Dec 22 '10 at 17:54
  • Umm...Zac...the OP is asking for advice on how they could have avoided the problem. CORRECT use of unit tests is my advice. INCORRECT use of unit tests is of course NOT what I am recommending. I mean...DUH!!! Jeesh! – Edward Strange Dec 22 '10 at 17:58
  • Unfortunately, C++ is not very friendly towards TDD. Main problem being the time it takes and also how hard it is to run a specific test. In my experience you really can't expect/demand C++ developers to follow a strict TDD. – Daniel Lidström Dec 22 '10 at 18:01
  • 2
    @Daniel - the only issues with TDD I've ever had in C++ have been poor design choices...which have pretty much been created by not doing TDD to begin with. – Edward Strange Dec 22 '10 at 18:03
  • @Noah That's so true, and will be a problem in any language. – Daniel Lidström Dec 22 '10 at 18:06
  • Unfortunately `Unit Tests` are unlikely to catch this type of bug. 'Unit Tests' are more like regressions tests they check the old functionality as defined still works not that new functionality does not screw things up. – Martin York Dec 22 '10 at 19:11
  • @Daniel: D has taken TDD into consideration as part of the language. – Martin York Dec 22 '10 at 19:13
  • Martin, bullshit - and for exactly the reason you claim they won't and then some. – Edward Strange Dec 22 '10 at 20:14
  • 1
    "A proper unit test for your object" - for that matter, a proper update of `operator==` would have turned it up, so I advise that :-). Problem seems to be someone has changed the class, but hasn't thought *at all* how that change affects comparisons. It's *possible* that any changes they made to the tests would have caught the problem. If they haven't changed the class interface, it's possible the existing tests would catch it. I don't think it's guaranteed even with good existing tests. Certainly their error means they would not have reviewed the tests that explicitly concern `operator==`. – Steve Jessop Dec 22 '10 at 22:37
  • How would you write a unit test that detects that new members aren't being tested in `operator==`? I really can't see that, please enlighten me (no sarcasm). – Xeo Sep 12 '12 at 21:05
  • @Xeo - you don't. You test state. So you'd create two different objects with different state based on the new variable and then verify that they're not considered equal. – Edward Strange Sep 12 '12 at 23:57
2

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.

Tim Welch
  • 21
  • 1
0

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.

villintehaspam
  • 8,540
  • 6
  • 45
  • 76
0

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)serialization
  • operator<
  • 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.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699