4101. LWG 117 loses the sign for negative NaN on some architectures

Section: 31.7.6.3.2 [ostream.inserters.arithmetic] Status: New Submitter: Jonathan Wakely Opened: 2024-05-10 Last modified: 2024-05-15

Priority: 3

View all other issues in [ostream.inserters.arithmetic].

View all issues with New status.

Discussion:

LWG 117 fixed insertion of a float into an ostream by requiring a cast to double. This gives a surprising result on RISC-V when inserting a negative NaN, because RISC-V floating-point instructions do not preserve the sign or payload of NaN values. This means that std::cout << -std::numeric_limits<float>::quiet_NaN() prints "nan" rather than "-nan" as most users probably expect.

11.3 section of the RISC-V ISA manual (20191213):

Except when otherwise stated, if the result of a floating-point operation is NaN, it is the canonical NaN. The canonical NaN has a positive sign and all significand bits clear except the MSB, a.k.a. the quiet bit. For single-precision floating-point, this corresponds to the pattern 0x7fc00000.

This is allowed by IEEE 754 (2019), as per section 6.3:

When either an input or result is a NaN, this standard does not interpret the sign of a NaN.

So it is standard-conforming for static_cast<double>(val) to lose the sign (and payload) of a NaN. This might also affect Apple M1 chips, if they use the ARMv8 default-NaN mode.

The current wording does not permit an implementation to use something like std::copysign(static_cast<double>(val), std::signbit(val) ? -1.0 : +1.0) to restore the sign bit. Should this be allowed? Maybe we should say that it's unspecified whether the cast to double preserves the sign of a NaN? If not, should we add a note about the surprising behaviour?

Previous resolution [SUPERSEDED]:

This wording is relative to N4981.

  1. Modify 31.7.6.3.2 [ostream.inserters.arithmetic] as indicated:

    -3- When val is of type float the formatting conversion occurs as if it performed the following code fragment:
    
      bool failed = use_facet<
        num_put<charT, ostreambuf_iterator<charT, traits>>
          >(getloc()).put(*this, *this, fill(),
            static_cast<double>(val)).failed();
    
    [Note ?: When val is a NaN value, the conversion to double can alter the sign and payload. — end note]

[2024-05-15; Reflector poll]

Set priority to 3 after reflector poll.

[2024-05-15; Peter Dimov provides improved wording]

Jonathan observes that the same problem exists for operator<<(extended-floating-point-type).

Proposed resolution:

This wording is relative to N4981.

  1. Modify 31.7.6.3.2 [ostream.inserters.arithmetic] as indicated:

    -3- When val is of type float the formatting conversion occurs as if it performed the following code fragment:
    
      bool failed = use_facet<
        num_put<charT, ostreambuf_iterator<charT, traits>>
          >(getloc()).put(*this, *this, fill(),
            static_cast<double>(val)dval).failed();
    
    where dval is val converted to type double as if by static_cast<double>(val), except that the sign and payload of NaN values may be preserved rather than discarded.