0

all.

So, I have created a Comparable base class (a la Java) which does the job of overloading comparison operators in C++. Here it is:

template <class T>
class Comparable {
public:
  bool operator== (const T& rhs) const { return this->compare(rhs) == 0; }
  bool operator!= (const T& rhs) const { return !(*this == rhs); }
  bool operator<  (const T& rhs) const { return this->compare(rhs) < 0; }
  bool operator<= (const T& rhs) const { return (*this == rhs) || (*this < rhs); }
  bool operator>  (const T& rhs) const { return this->compare(rhs) > 0; }
  bool operator>= (const T& rhs) const { return (*this == rhs) || (*this > rhs); }
protected:
  virtual int compare (const T& rhs) const = 0;
};

The subclass objects have both an ID, and data that needs to be a key for sorting. I've implemented each so that objects with the same ID return 0, and if they're not the same ID, they sort according to their data. Here's an example:

int Foo::compare(const Foo& rhs) const
{
    if (_id == rhs._id)
        return 0;
    // _value is integer data; comparison should sort on this
    if (_value == rhs._value)
    {
        // OK, same data, now sort on ID's
        return _id.compare(rhs._id);
    }
    // Sort on value
    return _value - rhs._value;
}

So far, so good, I think.

However, when I attempt to store Foo objects in a std::set container, the set will NOT eliminate duplicates. That is, there will still be objects in it that contain the same ID, even though that should be considered equal.

Does anyone have any idea what is going on?

EDIT: There are a lot of questions about why the code is designed this way.

There are two conditions I need to satisfy:

  1. Objects with the same ID must be considered the "same" object, regardless of value.
  2. Objects that do not have the same ID must be sorted according to their value (what that value is, depends upon the type of the object).

This is because the data that creates these objects is coming from an unverified source. If that source gives data with the same ID but a different value, that's invalid data.

EDIT 2: Regarding strict weak ordering for std::map. Let's say you have two Foo objects, A and B.

If their ID's are equal, then A < B is false, and B < A is false. If their ID's are not equal, then A < B is true or false depending upon their values, and B < A is the opposite of A < B.

Should this not satisfy the strict ordering rules?

In any case, if std::set uses the less-than operator (as it does by default), shouldn't it work as designed?

EDIT 3: std::set, not std::map. That was really stupid of me. Apologies.

Karl Giesing
  • 1,654
  • 3
  • 16
  • 24
  • 2
    Your base class is missing a virtual destructor. – chris May 11 '14 at 18:51
  • 1
    if (_id == rhs._id) return 0; This looks odd -- this would mean it says it's equal regardless of value, but you handle the ID compare later if the `value`s are equal. Which one is it? – Joe May 11 '14 at 18:51
  • Also, how does this differentiate between data that is the same ID but different values vs. data that has different IDs from sorting? Your function is ambiguous in this regard (I think; you don't show _id.compare) in that 2 items with the same ID whose value differs by 2, and 2 items whose ID that differs by 2 will be treated as equal. – Joe May 11 '14 at 18:54
  • 2
    Your definitions of <= and >= invoke `compare` twice, which seems like a bad idea. – Alan Stokes May 11 '14 at 18:54
  • Write some tests for `Foo:compare`. – Alan Stokes May 11 '14 at 18:56
  • @Joe: Objects with the same ID should return equal, regardless of value. That is by design. If they have the same ID, then they are supposed to be the same object. The data comes from an unverified source, so this may not always be the case - hence using a set to only store unique objects. – Karl Giesing May 11 '14 at 18:58
  • @AlanStokes: I did, that's how I know it's not working. The compare method works as designed. – Karl Giesing May 11 '14 at 18:59
  • 1
    You need better tests. `map` requires that < be a strict weak ordering; if not you get undefined behaviour. See e.g. http://stackoverflow.com/questions/979759/operator-and-strict-weak-ordering – Alan Stokes May 11 '14 at 18:59
  • 2
    Does your compare method implement a strict weak ordering? – juanchopanza May 11 '14 at 19:02
  • If ID is the key (and all things with same ID are the same), then how does value factor into your ordering at all? – Joe May 11 '14 at 19:02
  • @AlanStokes: I'm not sure what you mean by that. Objects with the same ID's return 0, which means < is false, > is false, == is true. Objects that do not have the same ID return something other than zero, which means == is false, < is true or false (depending on order), and > is the opposite of <. This is EXACTLY how I want my objects to be sorted. Where is the problem? – Karl Giesing May 11 '14 at 19:05
  • @KarlGiesing map will determine if an object is the same by seeing if A – Joe May 11 '14 at 19:17
  • @chris: I'm not actually using any ponters or references to the Comparable class, so it's not related to this issue. But it's a good point, and I'll add one. – Karl Giesing May 11 '14 at 19:29
  • 1
    "Strict weak ordering" is about consistency between comparisons of different objects - for example, if a < b, and b < c, then it must be true that a < c. Just making sure a < b is !(a >= b) doesn't begin to cover this. – Alan Stokes May 11 '14 at 19:29

2 Answers2

4

std::map does not determine duplicates via operator==. It uses operator<1, since it needs to use that anyway to determine order. Your operator< is broken. It needs to enforce a strict weak ordering.

The way in which your comparison fails is in the following property (called transitivity). For 3 objects, if A < B and B < C, then it must be the case that A < C.

So, consider three objects, A, B and C. A.id != B.id, but A.value < B.value, so A < B. Now, C.id == B.id (therefore C.id != A.id), but C.value < A.value. So C < A, and A < B. Therefore, C should be < B. But it's not, because C.id == B.id.

One way to do that would be to define your compare function like this:

int Foo::compare(const Foo& rhs) const
{
    if (_id < rhs._id)
        return -1;
    if (rhs._id < _id)
        return 1;

    return _value - rhs._value;
}

If you cannot use that, and you cannot find some other way to enforce a proper ordering, then you simply cannot use your objects as keys in a std::map.

1. If rhs is not less than lhs, and lhs is not less than rhs, then it is implied that they are equal. You can also provide an alternative functor as long as it also enforces a strict weak ordering.

Benjamin Lindley
  • 101,917
  • 9
  • 204
  • 274
  • That will sort by ID, and will only sort by value if those ID's are the same. That is exactly the opposite of what I am required to do. – Karl Giesing May 11 '14 at 19:15
  • 2
    @KarlGiesing: It was just an example. The point is that you'll have to come up with a way to define a strict weak ordering for your objects. If you are unable to do that, then you cannot use `std::map`. – Benjamin Lindley May 11 '14 at 19:20
  • 1
    The transitivity requirement is exactly the problem. Just confirmed it. Thank you. – Karl Giesing May 11 '14 at 19:56
3

If you don't have a strict weak ordering, you don't get to use map, and you can't complain if you do and it doesn't work. It's a precondition of map.

EDITED:

Your compare does not implement the semantics you specify - you should return 0 if _value == rhs._value in your second test.

But with that fix you still don't have a consistent ordering - see the example given by Benjamin Lindley in the comments. Essentially your ordering doesn't make sense - you either need to fix your semantics, or stop using standard library components that require a consistent ordering.

Alan Stokes
  • 18,815
  • 3
  • 45
  • 64
  • 1
    That is not sufficient. Consider three objects, `A`, `B` and `C`. `A.id != B.id`, but `A.value < B.value`, so `A < B`. Now, `C.id == B.id` (therefore `C.id != A.id`), but `C.value < A.value`. So `C < A`, and `A < B`. Therefore, `C` *should be* `< B`. But it's not, because `C.id == B.id`. – Benjamin Lindley May 11 '14 at 19:32
  • @Benjamin Thanks. I was struggling to come up with a counter example. I'll update. – Alan Stokes May 11 '14 at 19:33
  • @BenjaminLindley: That actually makes sense to me, I understand now. Make it an answer and you'll get my vote. – Karl Giesing May 11 '14 at 19:39
  • @KarlGiesing: I'll add it to my answer. – Benjamin Lindley May 11 '14 at 19:40