2

Requirements:

  1. container which sorts itself based on numerically comparing the keys (e.g. std::map)
  2. check existence of key based on float tolerance (e.g. map.find() and use custom comparator )
  3. and the tricky one: the float tolerance used by the comparator may be changed by the user at runtime!

The first 2 can be accomplished using a map with a custom comparator:

struct floatCompare : public std::binary_function<float,float,bool>
{
    bool operator()( const float &left, const float &right ) const
    {
        return (fabs(left - right) > 1e-3) && (left < right);
    }
};

typedef std::map< float, float, floatCompare > floatMap;

Using this implementation, floatMap.find( 15.0001 ) will find 15.0 in the map.

However, let's say the user doesn't want a float tolerance of 1e-3. What is the easiest way to make this comparator function use a variable tolerance at runtime? I don't mind re-creating and re-sorting the map based on the new comparator each time epsilon is updated.

Other posts on modification after initialization here and using floats as keys here didn't provide a complete solution.

Community
  • 1
  • 1
tcdaniel
  • 203
  • 1
  • 2
  • 11
  • 3
    You would have to create a new map with the new sorting criteria, and populate it with elements from the old map. But do you think your comparator satisfies a *strict weak ordering*? This is a requirement, without which your map is broken. – juanchopanza Jan 14 '14 at 15:48
  • 6
    A comparator like that won't work: associative containers require a strict ordering. You should be able to get what you want with the default ordering, using `lower_bound` (twice, for each end of the tolerance range) instead of `find`. That also gives your third requirement, since you can specify the tolerance for each lookup. – Mike Seymour Jan 14 '14 at 15:50
  • Comparator for sorting doesn't have so much to do with the comparator for tolerant searching! – leemes Jan 14 '14 at 15:51
  • If you don't mind recreating and resorting the map at runtime, then do _just that_. – Chad Jan 14 '14 at 15:51
  • What I mean: if you have a tolerance, you still want to sort the numbers according to the default `<`. Only if you look up a key you want to take the tolerance into account. – leemes Jan 14 '14 at 15:52
  • @MikeSeymour That solves the problem very well. Put that into an answer and you get my vote. – leemes Jan 14 '14 at 15:54
  • @juanchopanza, @Mike - I realize the ordering would be undefined in certain cases (e.g. where doubles are within tolerance of each other)... although, for my use case, I don't expect this to ever happen and can accept the ambiguity. @juanchopanza: so you suggest re-implementing any functions using `lower_bound`? That would include count(), lower_bound(), upper_bound(), equal_range()... – tcdaniel Jan 14 '14 at 16:06
  • 4
    To understand why strict ordering matters, consider inserting the values 14.9991, 15.0, and 15.0009 into your map. Depending on the order of insertion you might end up with 1 or 2 entries. Searches are likely to fail too, but that's harder to demonstrate. – Mark Ransom Jan 14 '14 at 16:09
  • @Mark - +1 for example. So perhaps there's a better way to accomplish my requirements e.g. a different type of container altogether? – tcdaniel Jan 14 '14 at 16:11
  • 1
    "undefined in certain cases" means your map cannot be guaranteed to work. In my book, that means it is broken. – juanchopanza Jan 14 '14 at 16:20
  • Your requirements are inconsistent with themselves. You need new requirements, not a new container. – tmyklebu Jan 15 '14 at 05:01

5 Answers5

8

You can't change the ordering of the map after it's created (and you should just use plain old operator< even for the floating point type here), and you can't even use a "tolerant" comparison operator as that may vioate the required strict-weak-ordering for map to maintain its state.

However you can do the tolerant search with lower_bound and upper_bound. The gist is that you would create a wrapper function much like equal_range that does a lower_bound for "value - tolerance" and then an upper_bound for "value + tolerance" and see if it creates a non-empty range of values that match the criteria.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
Mark B
  • 95,107
  • 10
  • 109
  • 188
  • Mark - about to accept this, but I now realize certain insertions may fail per Mark Ransom's comment to my question above. Is there a way to guard against this? – tcdaniel Jan 14 '14 at 16:13
  • 1
    @tc It won't happen if you use the standard comparator. Simply use default ordering and only use the tolerance when looking up values. – leemes Jan 14 '14 at 16:26
5

You cannot change the definition of how elements are ordered in a map once it's been instantiated. If you were to find some technical hack to do so (such as implementing a custom comparator that takes a tolerance that can change at runtime), it would evoke Undefined Behavior.

Your main alternative to changing the ordering is to create another map with a different ordering scheme. This other map could be an indexing map, where the keys are ordered in a different way, and the values arent the elements themselves, but an index in to the main map.

Alternatively maybe what you're really trying to do isn't change the ordering, but maintain the ordering and change the search parameters.

That you can do, and there are a few ways to do it.

One is to simply use map::lower_bound -- once with the lower bound of your tolerance, and once with the upper bound of your tolerance, just past the end of tolerance. For example, if you want to find 15.0 with a tolerance of 1e-5. You could lower_bound with 14.99995 and then again with 15.00005 (my math might be off here) to find the elements in that range.

Another is to use std::find_if with a custom functor, lambda, or std::function. You could declare the functor in such a way as to take the tolerance and the value at construction, and perform the check in operator().

Since this is a homework question, I'll leave the fiddly details of actually implementing all this up to you. :)

John Dibling
  • 99,718
  • 31
  • 186
  • 324
  • For tolerant searching, the ordering should not be changed... (I know the title is misleading and you answer the title; but you don't answer the actual problem, i.e. XY problem) – leemes Jan 14 '14 at 15:52
  • @leemes: Hmm, maybe I answered a question that wasn't asked. – John Dibling Jan 14 '14 at 15:53
  • @leemes the question does ask about changing the maps's order after creation, and mentions `map::find` as a means to find a key with tolerance. Probably an XY problem, but I think this answer is informative enough to merit a +1 from me. – juanchopanza Jan 14 '14 at 15:57
  • @leemes: How's that? – John Dibling Jan 14 '14 at 16:02
  • @John - understood. I mentioned being willing to re-create the map if absolutely necessary, but there must be a better way. – tcdaniel Jan 14 '14 at 16:04
1

You can't achieve that with a simple custom comparator, even if it was possible to change it after the definition, or when resorting using a new comparator. The fact is: a "tolerant comparator" is not really a comparator. For three values, it's possible that a < c (difference is large enough) but neither a < b nor b < c (both difference too small). Example: a = 5.0, b = 5.5, c = 6.0, tolerance = 0.6

What you should do instead is to use default sorting using operator< for floats, i.e. simply don't provide any custom comparator. Then, for the lookup don't use find but rather lower_bound and upper_bound with modified values according to the tolerance. These two function calls will give you two iterators which define the sequence which will be accepted using this tolerance. If this sequence is empty, the key was not found, obviously.

You then might want to get the key which is closest to the value to be searched for. If this is true, you should then find the min_element of this subsequence, using a comparator which will consider the difference between the key and the value to be searched.

template<typename Map, typename K>
auto tolerant_find(const Map & map, const K & lookup, const K & tolerance) -> decltype(map.begin()) {
    // First, find sub-sequence of keys "near" the lookup value
    auto first = map.lower_bound(lookup - tolerance);
    auto last = map.upper_bound(lookup + tolerance);

    // If they are equal, the sequence is empty, and thus no entry was found.
    // Return the end iterator to be consistent with std::find.
    if (first == last) {
        return map.end();
    }

    // Then, find the one with the minimum distance to the actual lookup value
    typedef typename Map::mapped_type T;
    return std::min_element(first, last, [lookup](std::pair<K,T> a, std::pair<K,T> b) {
        return std::abs(a.first - lookup) < std::abs(b.first - lookup);
    });
}

Demo: http://ideone.com/qT3JIa

leemes
  • 44,967
  • 21
  • 135
  • 183
1

Rather than using a comparator with tolerance, which is going to fail in subtle ways, just use a consistent key that is derived from the floating point value. Make your floating point values consistent using rounding.

inline double key(double d)
{
    return floor(d * 1000.0 + 0.5);
}
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • Agreed - I thought of this also and may in fact implement it this way. Of course how the rounding is done can be user-defined per my requirement. – tcdaniel Jan 14 '14 at 16:52
  • This fails to find values that are nearer to each other than the tolerance when they cross one of the artificial boundaries created by rounding. E.g., 1.234375 and 1.234619140625 differ by much less than the tolerance but are binned differently by this function. – Eric Postpischil Jan 14 '14 at 17:51
  • @EricPostpischil, that's unavoidable if you want to keep strict ordering. – Mark Ransom Jan 14 '14 at 19:08
  • @MarkRansom: Other answers have already shown how to keep strict ordering and provide the third requirement in a different way that does not have this problem. – Eric Postpischil Jan 14 '14 at 19:14
0

It may be better to leave the std::map class alone (well, partly at least), and just write your own class which implements the three methods you mentioned.

template<typename T>
class myMap{
 private:
   float tolerance;
   std::map<float,T> storage;

 public:
   void setTolerance(float t){tolerance=t;};
   std::map<float,T>::iterator find(float val); // ex. same as you provided, just change 1e-3 for tolerance
   /* other methods go here */
};

That being said, I don't think you need to recreate the container and sort it depending on the tolerance.

check existence of key based on float tolerance

merely means you have to check if an element exists. The position of the elements inside the map shouldn't change. You could start the search from val-tolerance, and when you find an element (the function find returns an iterator), get the next elements untill you reach the end of the map or untill their values exceed val+tolerance.

That basically means that the behavior of the insert/add/[]/whatever functions isn't based on the tolerance, so there's no real problem of storing the values.

If you're afraid the elements will be too close to eachother, you may want to start the searching from val, and then gradually increase the toleration untill it reaches the user desired one.

Paweł Stawarz
  • 3,952
  • 2
  • 17
  • 26