8

Related: what can I use as std::map keys?

I needed to create a mapping where specific key locations in space map to lists of objects. std::map seemed the way to do it.

So I'm keying a std::map on an xyz Vector

class Vector
{ 
  float x,y,z
} ;

, and I'm making a std::map<Vector, std::vector<Object*> >. So note the key here is not a std::vector, its an object of class Vector which is just a math xyz vector of my own making.

To produce a "strictly weak ordering" I've written the following overload for operator<:

  bool Vector::operator<( const Vector & b ) const {
    // z trumps, then y, then x
    if( z < b.z )
    {
      return true ;
    }
    else if( z == b.z )
    {
      if( y < b.y )
      {
        // z == b.z and y < b.y
        return true ;
      }
      else if( y == b.y )
      {
        if( x < b.x )
        {
          return true ;
        }
        else if( x == b.x )
        {
          // completely equal
          return false ;
        }
        else
        {
          return false ;
        }
      }
      else
      {
        // z==b.z and y >= b.y
        return false ;
      }
    }
    else
    {
      // z >= b.z
      return false ;
    }
  }

Its a bit long but basically makes it so any vector can consistently be said to be less than any other vector ((-1, -1, -1) < (-1,-1,1), and (-1, -1, 1) > (-1,-1,-1) for example).

My problem is this is really artificial and although I've coded it and it works, I am finding that it "pollutes" my Vector class (mathematically) with this really weird, artificial, non-math-based notion of "less than" for a vector.

But I need to create a mapping where specific key locations in space map to certain objects, and std::map seems the way to do it.

Suggestions? Out-of-box solutions welcome!!

Community
  • 1
  • 1
bobobobo
  • 64,917
  • 62
  • 258
  • 363
  • If you are using floats you maybe want to avoid this z == b.z. Try sth like abs(z-b.z) – InsertNickHere Aug 18 '10 at 18:35
  • This is sort of unrelated to your specific question (so I'll post it as a comment), but since you did ask for suggestions... The use of `==` to compare floating point values could be a problem. If two values only differ by roundoff errors, do you really want them treated as different locations? You might want to treat components within 'epsilon' of each other as equal, for the purpose of your comparison operator. – Jim Lewis Aug 18 '10 at 18:40
  • @InsertNick: Great minds think alike! (Yet fools rarely differ...) – Jim Lewis Aug 18 '10 at 18:41
  • @InsertNickHere: Comparison methods using absolute errors, relative errors all have their own pitfalls. There are methods that check the _units in last place_ (i.e. number of floats separating the compared two) which are slightly better. But then there are sub-normals and NaNs to take care of too ... – dirkgently Aug 18 '10 at 18:41
  • @Jim Lewis The problem with using an epsilon like this is that it makes the map behave differently depending on in which order the points are inserted. – Andreas Brinck Aug 18 '10 at 18:43
  • Using a delta in a map comparator sounds like the sort of thing that could actually wind up with a scenario where a < b and b < a, throwing the map completely out of whack. – Mark B Aug 18 '10 at 19:17
  • @InsertNickHere Actually I __don't__ want to use on epsilon difference here -- u is _near_ v -- its more expensive and all I need is a strict weak ordering out of this! – bobobobo Aug 18 '10 at 19:37
  • The way I am using the map is I'm certain Vectors I look up will be identical. – bobobobo Aug 18 '10 at 19:44
  • Also everyone who suggested using an EPSILON, you actually __will__ get a debug error `invalid operator<`. And you will ignore it for a couple of hours, then it will annoy you, then you will track it down to a SO comment. – bobobobo Sep 04 '10 at 02:04

5 Answers5

6

Instead of defining operator< for your key class, you can give the map a custom comparator. This is a function object that takes two arguments and returns true if the first comes before the second. Something like this:

struct CompareVectors
{
    bool operator()(const Vector& a, const Vector& b)
    {
        // insert comparison code from question
    }
};

typedef std::map<Vector, Value, CompareVectors> VectorValueMap;
Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • This is a very good solution which sidesteps the issue of defining an artificial meaning for `operator<` for Vectors. Do you still recommend this over a `hash_map` though? – bobobobo Aug 19 '10 at 16:08
  • A hash map would also be good, if you have an implementation of it. Note that it's not standard yet, and when it is it will be called `unordered_map`. – Mike Seymour Aug 19 '10 at 16:34
4

You can separate it from the class. Then specify it as the comparison operator for the std::map.

std::map<Vector,std::vector<Object*>,Compare>  data;

Where Compare is a function (or functor) that can compare tow Vector objects.
I also think you can simplify your Compare operation.

bool Compare<( const Vector& lhs, const Vector& rhs)
{
   // z trumps, then y, then x
   if( lhs.z < rhs.z )
   {    return true ;
   }
   else if (lhs.z > rhs.z)
   {    return false;
   }
   // Otherwise z is equal
   if( lhs.y < rhs.y )
   {    return true ;
   }
   else if( lhs.y > rhs.y )
   {    return false;
   }
   // Otherwise z and y are equal
   if ( lhs.x < rhs.x )
   {    return true;
   }
   /* Simple optimization Do not need this test
      If this fails or succeeded the result is false.
   else if( lhs.x > rhs.x )
   {    return false;
   }*/
   // Otherwise z and y and x are all equal
   return false;
}

Notice we test for less then greater and then fall through for equal. Personally I like the simplicity of this style. But I often see this being compressed like this:

bool Compare<( const Vector& lhs, const Vector& rhs)
{
    // Note I use three separate if statements here for clarity.
    // Combining them into a single statement is trivial/
    //
    if ((lhs.z < rhs.z)                                        ) {return true;}
    if ((lhs.z == rhs.z) && (lhs.y < rhs.y)                    ) {return true;}
    if ((lhs.z == rhs.z) && (lhs.y == rhs.y) && (lhs.x < rhs.x)) {return true;}

    return false;
}
Martin York
  • 257,169
  • 86
  • 333
  • 562
1

I think std::tr1::unordered_map is just what you need. No strict weak ordering will be required. GCC has a something similar in tr1 namespace as well. Or go for Boost.Unordered.

The unordered counterparts of the more pedestrian map or set gives you two advantages:

  • You don't need to define a less-than operator where none makes sense

  • Hash tables may perform better than balanced binary trees, the latter being the preferred method of implementing the ordered map or set structures. But that depends on your data access pattern/requirements.

So, just go ahead and use:

typedef std::tr1::unordered_map<Vector, std::vector<Object *> > VectorMap;

This makes use of a default hash function that takes care of insertion/search for your map.

PS: the > > thingy will be fixed in the upcoming standard and hence future compiler versions.

dirkgently
  • 108,024
  • 16
  • 131
  • 187
  • I like this answer but I am somewhat uncomfortable with using things from the `tr1` namespace. – bobobobo Aug 19 '10 at 16:11
  • Why, that is to say to do you any specific problems? TR1 is part of the standard that compliant compilers are supposed to implement. – dirkgently Aug 19 '10 at 17:00
1

It's normal that you find that your class is polluted by this. It's also polluted from a CS point of view.

The normal way of defining such an operator is through (potentially friend) free functions.

However the first question to ask yourself is: does it makes sense. The issue is that you have defined a method for your class that is only meaningful in a limited context but accessible everywhere. That's why the "pollution" feeling kicks in.

Now, if I were to need such mapping from a Vector to a collection of Objects, here are the questions I would ask myself:

  • Do I need the Vector to be ordered ? Yes: std::map, No: std::unordered_map or std::tr1::unodered_map or std::hash_map or boost::unordered_map.
  • Will this collection owns the Object ? Yes: boost::ptr_vector<Object> or std::vector< std::unique_ptr<Object> >, No: std::vector<Object*>

Now, in both cases (map and unordered_map), I will need something to transform my key. The collection provide a supplementary template argument which takes a Functor type.

Beware: as has been mentioned in another answer, floating point representation is awkward in a computer, therefore you will probably need to relax the meaning of equality and ignore the lower order digits (how many depends on your computations).

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
0

I think your approach is fine. If you're worried about polluting the Vector class, then I believe a stand-alone function will work just as well:

bool operator<( const Vector& lhs, const Vector& rhs )
{
    ...
}

But just a word of warning: what you're doing here is pretty risky. There are often errors in floating point calculations. Suppose you insert some point into your map. Then you calculate a point and check the map to see if it's there. Even if, from a strictly mathematical point of view, the second point is the same as the first, there's no guarantee that you'll find it in the map.

Peter Ruderman
  • 12,241
  • 1
  • 36
  • 58
  • If two points don't have exactly the same floating point coordinates they can't be said to be strictly mathematical the same. – Andreas Brinck Aug 18 '10 at 18:40
  • @Andreas: To be a little more formal about it: Suppose you have to functions f and g that generate a point. Given that f(x) = g(y), it does not follow that they will compare equal in the computer when you implement them using floating point arithmetic. – Peter Ruderman Aug 18 '10 at 18:55
  • I meant pollution of the `Vector` class from a _mathematical_ perspective - what does it mean mathematically for one vector to be "less than" another? Here I'm defining something in code that has no basis in mathematics. About the "risky" comment: This actually maps Vectors that are guaranteed to be identical -- no need to concern about _near_ here. – bobobobo Aug 18 '10 at 19:38
  • Well, I see your point about the mathematical strangeness of it. But from a computer science point of view, there's nothing wrong with defining a total ordering for any particular set. In the STL, the way you do that is by overloading the < operator. – Peter Ruderman Aug 18 '10 at 19:50