0

A set of pointers is reporting that it "contains" a brand-new object, even though this object was just created, and has a memory address which differs from those already in the set.

#include <iostream>
#include <set>
#include <vector>

using namespace std;

class foo {
  public:
    int x;
    foo(int x) : x(x) {}
};

int main() {
    vector<foo> my_vector;
    set<foo*, bool (*)(foo*, foo*)> my_set([](foo* left, foo* right) { return left->x < right->x; });

    my_vector.reserve(10);
    for (int i = 0; i < 10; i++) {
        my_vector.emplace_back(i);
        my_set.emplace(&my_vector[i]);
    }
    foo temp(4);
    if (my_set.count(&temp))
        cout << "But why?" << endl;
    return 0;
}

This behavior DOES NOT occur if temp is initialized using an integer outside the range [0, 10). It's as if the integer x is being used to determine equality, as opposed to just priority / precedence.

The behavior I want is that distinct objects (i.e., with distinct memory locations) always be treated as distinct objects in the set, and that the objects' values x be used to order the elements. How do I achieve this behavior? Thanks.

BD107
  • 159
  • 8
  • 1
    Your comparator doesn't compare pointers, but values they point to. Two distinct pointers may very well point to equivalent data. You want a comparator that compares the data, and then pointers themselves (in order to break ties). – Igor Tandetnik Nov 21 '20 at 23:22
  • Please help me understand. I thought that the comparator was ONLY used to determine precedence / priority / ordering, and not to determine membership / equality. If this is incorrect, how exactly is the comparator used? – BD107 Nov 21 '20 at 23:25
  • 1
    In your lambda, If both values x are equal, you want to compare the pointers – JVApen Nov 21 '20 at 23:25
  • `std::set` cannot contain equivalent elements, where "equivalent" is defined as "neither is smaller than the other" (using the provided comparator for "smaller"). The comparator is **primarily** used for efficient searching - the fact that elements end up in sorted order is essentially a side effect of implementation. – Igor Tandetnik Nov 21 '20 at 23:26
  • You might want to read the docs: https://en.cppreference.com/w/cpp/container/set – JVApen Nov 21 '20 at 23:26
  • @IgorTandetnik interesting. Is there a way to use pointer-equality for _equality_, and the comparator for comparison? – BD107 Nov 21 '20 at 23:39
  • The whole reason `std::set` can implement things like `count` and `find` efficiently is because it uses the comparator for looking up elements. Internally, it's a balanced binary search tree. You could use `std::find`, as in `if (std::find(my_set.begin(), my_set.end(), &temp) != my_set.end()) ...` - this performs a linear scan using "normal" equality. But then you kinda defeat the point of using `std::set` in the first place. – Igor Tandetnik Nov 21 '20 at 23:41

3 Answers3

1

In a std::set, for two values L and R, if neither L<R nor R<L, they are considered equal. There is no separate of "priority/ordering" and equality, as you thought there might be.

How do I achieve this behavior?

Use a container other than std::set. For example a vector which you keep sorted.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • But my lambda only defines what it means for L < R. How would the set even interpret the meaning of R < L, when I have provided no such lambda? – BD107 Nov 21 '20 at 23:27
  • @BD107: L and R are just placeholder names. Call them Bob and Sally. If you define the lambda comparator for Bob and Sally, the `std::set` implementation can invoke it for `(Bob, Sally)` or `(Sally, Bob)` without asking you for permission. It just passes the arguments in the other order. – John Zwinck Nov 21 '20 at 23:29
  • Hmm. So the comparator is used to determine precedence _and_ equality / membership. Do you think I could achieve the desired behavior using a `multiset`? Suppose that I can guarantee each object `emplace`d will have a unique address. – BD107 Nov 21 '20 at 23:33
  • If I used a `vector`, then I'd have to implement binary search and insertion myself in order to keep it sorted. What if I used a `multiset`? – BD107 Nov 21 '20 at 23:35
  • You can try using a `multiset` but I am quite sure you will find it does not do what you want, because again it considers equality to be a concept built on top of ordering (if X is not before Y and Y is not before X, then X and Y are equivalent). – John Zwinck Nov 21 '20 at 23:43
1

Thanks all for the comments. Here is a solution that achieves the desired behavior:

int main() {
    set<foo*, bool (*)(foo*, foo*)> my_set([](foo* left, foo* right) {
        if (left->x == right->x)
            return std::less<foo*>{}(left, right);
        return left->x < right->x;
    });
    // ....
    return 0;
}

EDITED to reflect @Igor Tandetnik's helpful comment!

BD107
  • 159
  • 8
  • 1
    Danger, Will Robinson. `left < right` exhibits undefined behavior unless the two pointers point into the same array. To compare two arbitrary pointers, use `std::less{}(left, right)` – Igor Tandetnik Nov 22 '20 at 00:02
  • @IgorTandetnik So you mean the custom comparator should be removed because the default comparator of [`std::set`](https://en.cppreference.com/w/cpp/container/set) is `std::less`? – MikeCAT Nov 22 '20 at 00:05
  • No, I'm saying that you need to use `std::less` as shown, rather than the built-in operator `<`, when comparing two pointers that may not point into the same array. The built-in operator is only defined on pointers into the same array, whereas `std::less` defines a total order. – Igor Tandetnik Nov 22 '20 at 00:07
0

You could first check for equality in your lambda. If this check fails you can calculate the order relationship...

Bernd
  • 2,113
  • 8
  • 22