Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are IEEE floats valid key types for std::map and std::set?

Tags:

Background

The requirement for a comparator on the key type of an associative container (for example std::map) is that it imposes a strict weak order on the elements of the key type.

For a given comparator comp(x, y) we define equiv(x, y) = !comp(x, y) && !comp(y, x).
The requirements for comp(x, y) being a strict weak order are

  1. Irreflexibility (!comp(x, x) for all x)
  2. Transitivity of the ordering (if comp(a, b) and comp(b, c) then comp(a, c)).
  3. Transitivity of equivalence (if equiv(a, b) and equiv(b, c) then equiv(a, c))

std::less<float> (the default comparator) uses operator<, which does not create a strict weak order because of NaN. Because x < NaN and NaN < x are false for all x, NaN is equivalent to all floats under this comparator, this breaks condition #3: equiv(1.0, NaN) and equiv(NaN, 2.0) but not equiv(1.0, 2.0). For IEEE floats except NaN, it is a strict weak order (where each number has its own equivalence class except for 0 and -0).

The question

Does this mean that one is not allowed by the C++ standard to use IEEE floats (and (long) doubles) as a key type in an associative container because of the above issue, even if I make sure that NaN never gets inserted into the container? I'm not quite sure about the "elements of Key" wording in the standard—if it means all possible elements or just the elements that end up in the container.

Note: The question is not about issues wrt. truncation/rounding, I'll likely post a different question regarding that soon.

Update:

Sigh. I should've posed the question without specifying float, I just thought it was a nice example.

The question really is: Is it allowed to use a comparator that only imposes a strict weak order on the elements that get put into the container, not all possible instances of the key type? Please don't just answer "yes" or "no", I'd like some references to the standard / prior discussions about this / an answer from a commitee member or something.

like image 816
etarion Avatar asked Jan 27 '11 12:01

etarion


2 Answers

I suspect the restrictions should be taken as referring to the relation's behavior on the values actually used as keys, not necessarily on all values of the type. Don't have time at the moment to go through the standard looking for "smoking gun" language that refers to actual container elements rather than all values of the type.

Similar case: what if a comparator (for a container of pointers or smart pointers) calls a virtual function, and somebody links a derived class of the type it compares, which overrides the virtual function in a way that makes the comparator not a strict weak order? Does the program become undefined even if nobody ever actually uses that derived class?

If in doubt, you can support NaN with a comparator that is a strict weak order:

bool operator()(double a, double b) {     if ((a == a) && (b == b)) {         return a < b;     }     if ((a != a) && (b != b)) return false;     // We have one NaN and one non-NaN.     // Let's say NaN is less than everything     return (a != a) } 

The last two lines "optimize" to return (b == b);, although I'm not sure the comment optimizes with it.

I think Tomalak has convinced me the language does say the whole type needs to be ordered.

This makes little sense, since a map doesn't conjure values out of nowhere, it only uses values that it's given (and copies of them), but the question is about the rules, and them's the rules as far as I know. C++0x is the same. I wonder if there's a defect report, or any point submitting one.

It's also annoying in that on (very rare) systems where std::less is slow for pointers, you can't use < as the comparator in a map of pointers, even if you know that the pointers are all to elements of the same array. Shame.

Another option is to use the following class as the key type, so keys are checked for NaN only on entry to the map, not on every comparison as above.

struct SaneDouble {     double value;     SaneDouble (double d) : value(d) {         if (d != d) throw std::logic_error();     }     static friend bool operator<(SaneDouble lhs, SaneDouble rhs) {         return lhs.value < rhs.value;     }     // possibly a conversion to double }; 

This raises another question - clearly someone could create a SaneDouble and then set its value to NaN (assuming the implementation lets them get one from somewhere without crashing). So are "elements of SaneDouble" strict-weak-ordered or not? Does my half-hearted attempt to create a class invariant in the constructor make my program undefined even if nobody actually breaks the invariant, simply because they could and therefore the results of doing so are "elements of SaneDouble"? Is it really the intention of the standard that the program's behavior is defined if and only if value is marked private? Does the standard actually define anywhere what "the elements" of a type are?

I wonder whether we should interpret "elements of Key" to mean that the comparator induces a strict weak order on some elements of Key. Presumably including the ones actually used. "I have doughnuts" doesn't mean I have every doughnut. It's a stretch, though.

like image 73
Steve Jessop Avatar answered Sep 30 '22 22:09

Steve Jessop


In short: this is fine (in the sense that your question is about).

If you prevent the values (i.e. NaN) that wouldn't satisfy the ordering requirements, then the behaviour is entirely defined.

like image 32
Oliver Charlesworth Avatar answered Sep 30 '22 21:09

Oliver Charlesworth