简体   繁体   中英

std::map with keys that have no ordering

Which approach should be taken if I want to have a std::map using a user defined object as key?

Let's consider this minimal bogous code (which compiles but won't run correctly):

#include <map>

class Object
{
public:
  int x, y;
  Object(int x, int y) : x(x), y(y) {};
  bool operator<(const Object& o) const { return 1;};   // bogus just to make compiler happy
};

int main()
{
  Object o1(1, 2), o2(3, 4);
  std::map<Object, int> objmap;
  objmap.insert(std::make_pair(o1, 11));
  objmap.insert(std::make_pair(o2, 22));
}

In order to be able to use an object as key in a std::map , we need a < operator.

So far so good, but what can be done if the objects can't be compared in terms of "less" or "greater", but only in terms of "different"?

Your objects have to respect strict total order . That means, most importantly, your < relation must be irreflexive and transitive. In your case, consider just using std::tuple, which already provides a functioning operator< with lexicographical ordering.

This would be "a" correct ordering, but most likely not at all what you would want:

friend bool operator<(Object const& a, Object const& b) {
  return a.x < b.x; // probably not what you want, but legal
}

Because it would mean that an Object{1,100} and Object{1,101} would result in std::map assuming they are the same key (unless you want exactly that).

However, you can actually break map, if you provide the following:

friend bool operator<(Object const& a, Object const& b) {
  return a.x <= b.x; // this will break map
}

because it is not irreflexive (ie x < x will not yield false).

In general you have to consider all properties of your object, in order to avoid aliasing (ie map considers two distinct objects equal). Unless you show us your Object , there is no way to show you how to implement a correct operator< .


In most cases std::unordered_map is a better (and more efficient) choice.

So far so good, but what can be done if the objects can't be compared in terms of "less" or "greater", but only in terms of "different"?

You can generally synthesise an ordering, eg

bool operator<(const Object& o) const { return std::tie(a, b) < std::tie(o.a, o.b); }

Incidentally, this construction is what C++20's default comparison will use.

auto operator<=>(const Object& o) const = default;

As here there is no total order relationship between Object s, one possibility is to use unorded_map instead of map .

Therefore the object class needs at least an overloaded == operator and a custom hash function:

#include <unordered_map>
#include <iostream>

class Object
{
public:
  int x, y;
  Object(int x, int y) : x(x), y(y) {};

  bool operator==(const Object& o) const
  {
    std::cout << "equal hashes, using == operator on (" << x << "," << y << ")\n";
    auto isequal = x == o.x && y == o.y;
    std::cout << "objects are " << (isequal ? "" : "not ") << "equal\n";
    return isequal;
  };

  friend std::ostream& operator<<(std::ostream& os, const Object& o);
};

std::ostream& operator<<(std::ostream& os, const Object & o)
{
  os << "(" << o.x << "," << o.y << ")";
  return os;
}

struct ObjectHash {
  size_t operator()(const Object& o) const
  {
    std::cout << "hashing (" << o.x << "," << o.y << ")\n";;
    return o.x + o.y;  // purposly bad hash function, so we get collisions
                       // (o.x * 127) ^ o.y  would be better, still not ideal
  }
};


int main()
{
  Object o1(1, 2);
  Object o2(3, 4);  // different from o1, o3 and o4
  Object o3(1, 2);  // equal to o1
  Object o4(2, 1);  // different from o1, o2 and o3 but same hash as o1

  std::unordered_map<Object, int, ObjectHash> objmap;
  std::cout << "Inserting " << o1 << "\n"; objmap.insert(std::make_pair(o1, 11));
  std::cout << "Inserting " << o2 << "\n"; objmap.insert(std::make_pair(o2, 22));
  std::cout << "Inserting " << o3 << "\n"; objmap.insert(std::make_pair(o3, 33));
  std::cout << "Inserting " << o4 << "\n"; objmap.insert(std::make_pair(o4, 33));
}

The std::cout s are there for illustration purposes.

Output:

Inserting (1,2)
hashing (1,2)
Inserting (3,4)
hashing (3,4)
Inserting (1,2)
hashing (1,2)
equal hashes, using == operator on (1,2)
objects are equal
Inserting (2,1)
hashing (2,1)
equal hashes, using == operator on (2,1)
objects are not equal

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM