Hi all,
I posted a variation of this question on SO [1] but the answers and discussions therein made me even more confused. For the sake of self-containedness important points will be repeated here.
It's well known that < does not induce a strict weak ordering on the set of (IEEE-754) double values and the blame lies on NaNs. (Does it? More on that latter.) This poses a practical problem for associative containers and much has been said about this but let's consider something much simpler: std::min. (Quotes refer to n4800 [2] and are abbreviated for the sake of brevity.)
[alg.min.max]
template<class T>
constexpr const T& min(const T& a, const T& b);
1 Requires: [...] type T shall be Cpp17LessThanComparable (Table 24).
2 Returns: The smaller value.
3 Remarks: Returns the first argument when the arguments are equivalent. [...]
Cpp17LessThanComparable is defined in Table 24 which states that < is a strict weak ordering relation. The four properties that define strict ordering (e.g. in Wikipedia [3]) go along the line of "for all x in S, ...". Now the confusion starts.
Some people (me included) follow a, say, more theoretical approach and understand S as the set of all possible double values. We deduce that < is not a strict weak ordering on this set and thus, double is not Cpp17LessThanComparable. For us, instantiating std::min with T = double is UB, in particular, std::min(0.0, 1.0) is undefined. Please, do not get me wrong! One thing that everybody agrees is the intention that std::min(0.0, 1.0) must return 0.0. I'm simply arguing that, perhaps, the wording in the Standard is not precise enough to translate the intention into a indisputable specification.
Other people follow a, say, more pragmatical approach and see the Cpp17LessThanComparable requirement on T as applying to the "values which are passed to the algorithm". What I grasp from SO posts is that for them std::min(0.0, 1.0) is well defined (it's 0.0) but std::min(0.0, NaN) is not. The underlying idea seems to be that "everything is fine when NaNs are not involved".
We get to two questions:
1) Is the theoretical group right: instantiating std::min for T = double is UB?
2) Is the pragmatical group right: std::min(0.0, 1.0) well defined and std::min(0.0, NaN) not?
IMHO, the pragmatic approach (although, I insist, I agree with its intention) is too vague and leaves too many open questions. The mathematical definition of strict weak ordering doesn't mention any algorithm. Would it make sense for double to Cpp17LessThanComparable for algorithm A and not for algorithm B? Does "values passed to the algorithm" mean that double being Cpp17LessThanComparable depends on the run-time values of a and b (the two argumens in the declaration above)? If so, then what's the point of requiring, for instance, transitivity of < when only two values matter and there's not a third one to be compared against? Also, if only a and b matter, why would be std::min(0.0, NaN) undefined given that < is a strict weak ordering on {0.0, NaN}.
The Standard does give indications of the intention to say "everything is fine when NaNs are not involved". For instance, in the specification of std::clamp:
template<class T>
constexpr const T& clamp(const T& v, const T& lo, const T& hi);
1 Requires: The value of lo shall be no greater than hi. [...] type T shall be Cpp17 LessThanComparable (Table 24).
2 Returns: lo if v is less than lo, hi if hi is less than v, otherwise v.
3 [Note: If NaN is avoided, T can be a floating-point type.— end note]
The Note above (recall that notes are not normative) conveys the idea but I'm still not convinced this is the right way of allowing float-point types.
Another clue comes from [structure.requirements]/8 which came into the WD from P0898R0 [4] during the "conceptification" of the standard library:
8 Required operations of any concept defined in this document need not be total functions; that is, some arguments to a required operation may result in the required semantics failing to be satisfied. [Example: The required < operator of the StrictTotallyOrdered concept (17.5.4) does not meet the semantic requirements of that concept when operating on NaNs.— end example] This does not affect whether a type satisfies the concept.
Again it's only a note and it applies to concepts and not other named requirements like Cpp17LessThanComparable.
In any case, excluding NaN doesn't seem to be the right approach (at least as far as std::min is concerned).
First, as pointed out by Barry in [1] the same issue might occurs with other types/operator < which do not verify the Cpp17LessThanComparable requirement. How could one precisely define the rogue values that algorithms need to avoid (like NaNs for double)?
Second, as I pointed out in [1], one can argue that NaNs are not the issue but other double vales are. Specifically, recall that there's more than one NaN (exactly 2^52 - 1 values carrying different payloads). If S is the set of all NaN double values, then it can be shown that < is a strict weak ordering on S. Therefore, from the mathematical point of view, there would be no problem if Note 3 in the std::clamp's specification said "If non NaN are avoided, T can be a floating-point type".
FWIW, the resolution of Library Issue 2239 [5] removed the Cpp17LessThanComparable requirement for the version of std::min that takes a Compare but left it for the one using < because the Remarks element mentions equivalence.
The simplest way to specify std::min would be through code (I'm sure my Standardese is wrong but you get the idea):
template<class T>
constexpr const T& min(const T& a, const T& b);
1 Returns: a < b ? a : b provided this expression is well-formed.
There's no need for requiring T to be Cpp17LessThanComparable and there's no need to mention equivalence.
If I understand it correctly, as a matter of principle, the Standard avoids defining semantics through code to avoid giving the impression of enforcing the implementation. However, [structure.requirements]/7 already states that when C++ code is used to present semantic requirements it doesn't mean that's the way it must be implemented. That would be my favorite specification honoring std::min as a replacement for the macro with the same name. If sticking to the principle is really desirable, then a textual approach similar to std::clamp and could be used:
1 Returns: a if a is less than b, otherwise b.
(I would also suggest to remove Note 3 from std::clamp's specification.)
I have also comments regarding keys in associative containers but this post is getting too long and I want to hear your thoughts for now.
Thanks and regards,
Cassio.
[1]
https://stackoverflow.com/questions/55153210/do-stdmin0-0-1-0-and-stdmax0-0-1-0-yield-undefined-behavior[2]
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/n4800.pdf[3]
https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings[4]
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0898r0.pdf[5]
http://cplusplus.github.io/LWG/lwg-defects.html#2239