lmi
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [lmi] Shouldn't std::max() and std::min() be commutative?


From: Vadim Zeitlin
Subject: Re: [lmi] Shouldn't std::max() and std::min() be commutative?
Date: Sat, 27 Feb 2021 16:32:47 +0100

On Sat, 27 Feb 2021 13:13:47 +0000 Greg Chicares <gchicares@sbcglobal.net> 
wrote:

GC> Surely it is possible to implement std::max() and std::min() as
GC> commutative operations [below], so I wondered whether it is not
GC> therefore at least a QoI issue that libstdc++
GC>   $g++ --version
GC>   g++ (Debian 10.2.1-6) 10.2.1 20210110
GC> does not.

 FWIW clang 10 libc++ and MSVS 19 STL produce exactly the same results as
g++/libstdc++.

GC> The answer is that libstdc++ follows the standard, e.g.,
GC> C++20 (n4861) [alg.min.max / 3]:
GC>   "Remarks: Returns the first argument when the arguments are equivalent."
GC> Is it not therefore a defect of the standard that it prescribes the
GC> jarring and unnecessary behavior demonstrated below? What might they
GC> have been thinking--that a commutative implementation such as this
GC> quick sketch for type double:
GC> 
GC>     #include <cmath>
GC> 
GC>     constexpr bool operator()(T const& lhs, T const& rhs) const
GC>     {
GC>         if(0.0 == lhs && 0.0 == rhs)
GC>             {
GC>             return std::signbit(rhs) < std::signbit(lhs);
GC>             }
GC>         else
GC>             {
GC>             return lhs < rhs;
GC>             }
GC>     }
GC> 
GC> would be too slow?

 I think they were thinking about user-defined types with some weird
comparison definition, but I really don't know.

GC> Demonstration:
GC> 
GC> #include <algorithm>
GC> #include <cfloat>
GC> 
GC> int main()
GC> {
GC>     std::cout << std::max( 0.0, -0.0) << std::endl; // prints 0
GC>     std::cout << std::min( 0.0, -0.0) << std::endl; // prints 0
GC>     std::cout << std::max(-0.0,  0.0) << std::endl; // prints -0
GC>     std::cout << std::min(-0.0,  0.0) << std::endl; // prints -0
GC> 
GC>     double n = DBL_TRUE_MIN;
GC>     double a =  0.1 * n;
GC>     double b = -0.1 * n;
GC>     std::cout << a << std::endl; // prints 0
GC>     std::cout << b << std::endl; // prints -0
GC>     std::cout << std::max(a, b) << std::endl; // prints 0
GC>     std::cout << std::min(a, b) << std::endl; // prints 0
GC>     std::cout << std::max(b, a) << std::endl; // prints -0
GC>     std::cout << std::min(b, a) << std::endl; // prints -0
GC> 
GC>     return 0;
GC> }

 I hoped to provide a simple solution to the problem by advising to use
std::f{min,max} instead of std::{min,max}, as these functions are supposed
to behave exactly as you'd expect them to.

 But after running "%s/std::m/fm/" and adding <cmath> inclusion (and also
<iostream>, which is, of course, also required in the original example),
I've discovered, to my dismay, that they only behave correctly with MSVC,
out of the 3 compilers tested, please see the summary table below
(hopefully U+2227 and U+2228 below that I've used for making the headers
more compact are readable, but if not, all the odd columns are for max and
the non-zero even ones are for min):

    Compiler     | 0∨-0 | 0∧-0 | -0∨0 | -0∧0 | a∨b | a∧b | b∨a | b∧a |
    ------------------------------------------------------------------
    MSVC         |   0  |  -0  |   0  |  -0  |  0  | -0  |  0  |  -0 | [OK]
    gcc          |   0  |  -0  |   0  |  -0  | -0  | -0  |  0  |   0 |
    gcc -fRM     |   0  |  -0  |   0  |  -0  |  0  | -0  |  0  |  -0 | [OK]
    gcc -O2      |   0  |  -0  |   0  |  -0  |  0  | -0  |  0  |  -0 | [OK]
    gcc -O2 -fRM |   0  |  -0  |   0  |  -0  | -0  | -0  |  0  |   0 |
    clang        |   0  |   0  |  -0  |  -0  |  0  |  0  | -0  |  -0 |
    clang -fRM   |  -0  |  -0  |   0  |   0  | -0  | -0  |  0  |   0 |

As you can see, by default, i.e. without any flags, gcc does return the
expected values for the literal arguments, but not when using the
variables. Luckily, when using -frounding-math switch, that we do use, we
get the expected results for everything. Moreover, even just using -O2 is
also enough to get the expected results. However, and very unfortunately,
using both -O2 and -frounding-math somehow reverts to the default behaviour
and the results are different from the expected ones again.

 With clang things are even worse, if possible, as it doesn't give the
expected results even for constant values, whether -frounding-math is used
or not, although it does change them (-O2 does not however).


 Note that I've been careful to use "unexpected" rather than "incorrect"
results everywhere because these functions are _not_ required to
distinguish between +0 and -0. For example,
https://en.cppreference.com/w/c/numeric/math/fmax says:

> This function is not required to be sensitive to the sign of zero,
> although some implementations additionally enforce that if one argument
> is +0 and the other is -0, then +0 is returned. 

Similarly, a footnote in F.10.9.2 of C11 says

> Ideally, fmax would be sensitive to the sign of zero, for example
> fmax(-0.0, +0.0) would return +0; however, implementation in software
> might be impractical. 


 So, to summarize, the behaviour of f{min,max}() is a QoI issue and,
unfortunately, the only compiler dealing with it satisfactorily is MSVC.
And this means that there is no standard library function that would
reliably compute min(0,-0) as being -0 etc and that if such a function is
really needed in lmi, we have no choice but to write our own.

 It's not really clear to me if we actually do need this function or if
this was just an oddity you've noticed and decided to investigate. I live
in superstitious horror of negative zeroes, NaNs and other strange FP
beasts, so I'd prefer to avoid them entirely, if possibly, rather than
relying on getting the expected results when using them, but I could be
missing some reason for which they're actually required in lmi, of course.

 Please let me know if you think there is anything worth investigating
further here,
VZ

Attachment: pgpM2BjocX8lJ.pgp
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]