As far as I can tell, the MSVC implementation of signaling_NaN
does not comply with IEEE 754-2019, the latest version of the IEEE floating-point standard.
Unfortunately, I do not have a copy of the IEEE standard, so I must rely on Wikipedia. In particular, Wikipedia states that the IEEE standard requires, in a quiet NaN, that the MSB of the significand be set to 1
. The same bit should be set to 0
, for signaling NaNs.
In MSVC, function std::numeric_limits<float>::signaling_NaN()
sets it to 1
. (See the demonstration program below.) Under IEEE 754-2019, that indicates a quiet NaN.
In addition, the MSVC implementation of signaling_NaN
is inconsistent: types float
and double
are not treated analogously. Type double
handles signaling_NaN
as specified by IEEE 754-2019. Type float
does not.
I am posting this question to get a second opinion: Have I missed something? Is Wikipedia wrong?
According to Wikipedia, the 2019 revision of IEEE 754 requires that:
For binary interchange formats, the most significant bit of the significand field is exclusively used to distinguish between quiet and signaling NaNs. Moreover, it should be an is_quiet flag. That is, this bit is non-zero if the NaN is quiet, and zero if the NaN is signaling.
Earlier versions of the IEEE standard do not contain this requirement. Nevertheless, it was the method used in many implemetations. Under earlier versions of the standard (per Wikipedia), the bits used to indicate quiet and signaling NaN are implementation dependent.
The fact that Microsoft does not follow IEEE 754-2019 can be explained by asserting that Microsoft is following an earlier version of IEEE 754.
The fact that Microsoft treats float
and double
differently, however, suggests that something went wrong somewhere. It may be that an early version of MSVC introduced the inconsistency, and now Microsoft feels that it cannot correct it, without breaking existing code bases.
Or, perhaps, Microsoft does not know that an inconsistency exists.
I ran the following program on MSVC, g++, and Clang. You can see the demo at Compiler Explorer.
Clang and g++ both adhere to IEEE 754-2019. Of the three, only MSVC does not.
Here is the output from MSVC. It shows that std::numeric_limits<float>::signaling_NaN()
sets the is_quiet bit to 1
, thus indicating a quiet NaN.
It also shows that std::numeric_limits<double>::signaling_NaN()
sets the is_quiet bit to 0
, as required under IEEE 754-2019.
Look for the lines that say, "is_quiet."
Predefined Macros
__cplusplus : 199711
_MSVC_LANG : 202302
_MSC_VER : 1943
_MSC_FULL_VER : 194334810
_MSC_BUILD : 0
========
float
========
IEEE 754 Support
std::numeric_limits< float >::is_iec559 : true
std::numeric_limits< float >::has_quiet_NaN : true
std::numeric_limits< float >::has_signaling_NaN : true
std::numeric_limits< float >::quiet_NaN()
bits : 01111111110000000000000000000000
exponent : _11111111_______________________
significand : _________10000000000000000000000
is_quiet : _________1______________________
std::numeric_limits< float >::signaling_NaN()
bits : 01111111110000000000000000000001
exponent : _11111111_______________________
significand : _________10000000000000000000001
is_quiet : _________1______________________ <===== Whoops!
========
double
========
IEEE 754 Support
std::numeric_limits< double >::is_iec559 : true
std::numeric_limits< double >::has_quiet_NaN : true
std::numeric_limits< double >::has_signaling_NaN : true
std::numeric_limits< double >::quiet_NaN()
bits : 0111111111111000000000000000000000000000000000000000000000000000
exponent : _11111111111____________________________________________________
significand : ____________1000000000000000000000000000000000000000000000000000
is_quiet : ____________1___________________________________________________
std::numeric_limits< double >::signaling_NaN()
bits : 0111111111110000000000000000000000000000000000000000000000000001
exponent : _11111111111____________________________________________________
significand : ____________0000000000000000000000000000000000000000000000000001
is_quiet : ____________0___________________________________________________
// app23.signaling_nan.main.cpp
// Demonstration Program for Stack Overflow Question
// https://stackoverflow.com/q/79707504/22193627
#include <bit> // bit_cast
#include <cstddef> // size_t
#include <cstdint> // uint32_t, uint64_t
#include <iostream> // cout, ostream
#include <limits> // numeric_limits
#include <string> // string
#include <string_view> // string_view
namespace
{
//==================================================================
// put_predefined_macros
//==================================================================
void put_predefined_macros(std::ostream& log) {
log << "Predefined Macros\n"
<< " __cplusplus : " << __cplusplus << '\n';
#if defined(_MSC_VER)
// Compile with: /std:c++23preview
log << " _MSVC_LANG : " << _MSVC_LANG << '\n'
<< " _MSC_VER : " << _MSC_VER << '\n'
<< " _MSC_FULL_VER : " << _MSC_FULL_VER << '\n'
<< " _MSC_BUILD : " << _MSC_BUILD << '\n';
#endif
#if defined(__GNUC__)
// Compile with: -std=c++23
log << " __GNUC__ : " << __GNUC__ << '\n'
<< " __GNUC_MINOR__ : " << __GNUC_MINOR__ << '\n'
<< " __GNUC_PATCHLEVEL__ : " << __GNUC_PATCHLEVEL__ << '\n';
#endif
#if defined(__clang_major__)
// Compile with: -std=c++23
log << " __clang_major__ : " << __clang_major__ << '\n'
<< " __clang_minor__ : " << __clang_minor__ << '\n'
<< " __clang_patchlevel__ : " << __clang_patchlevel__ << '\n';
#endif
log << "\n\n\n";
}
//==================================================================
// isolated_bits
//==================================================================
template< typename bits_type >
std::string isolated_bits(
bits_type const b,
std::size_t const begin,
std::size_t const end)
{
std::size_t const n_bits{ sizeof(bits_type) * 8uz };
std::string s;
s.reserve(n_bits);
for (auto n{ n_bits }; n--;) {
char const c{
n < begin || n > end ? '_'
: (b >> n) & 1u ? '1' : '0'
};
s.push_back(c);
}
return s;
}
//==================================================================
// exponent_bits
//==================================================================
template< typename bits_type >
std::string exponent_bits(bits_type const b) {
return
sizeof(bits_type) == 4uz ? isolated_bits(b, 23uz, 30uz)
: sizeof(bits_type) == 8uz ? isolated_bits(b, 52uz, 62uz)
: "";
}
//==================================================================
// significand_bits
//==================================================================
template< typename bits_type >
std::string significand_bits(bits_type const b) {
return
sizeof(bits_type) == 4uz ? isolated_bits(b, 0uz, 22uz)
: sizeof(bits_type) == 8uz ? isolated_bits(b, 0uz, 51uz)
: "";
}
//==================================================================
// is_quiet_bit
//==================================================================
template< typename bits_type >
std::string is_quiet_bit(bits_type const b) {
return
sizeof(bits_type) == 4uz ? isolated_bits(b, 22uz, 22uz)
: sizeof(bits_type) == 8uz ? isolated_bits(b, 51uz, 51uz)
: "";
}
//==================================================================
// test_NaN
//==================================================================
template< typename NumT, typename bits_type >
void test_NaN(
std::ostream& log,
std::string_view heading,
NumT const nan)
{
auto const bits{ std::bit_cast<bits_type>(nan) };
log << std::format(
"{}\n"
" bits : {:0>{}b}\n"
" exponent : {}\n"
" significand : {}\n"
" is_quiet : {}\n"
"\n"
, heading
, bits
, sizeof(bits_type) * 8uz
, exponent_bits(bits)
, significand_bits(bits)
, is_quiet_bit(bits)
);
}
//------------------------------------------------------------------
template< typename NumT, typename bits_type >
void test_NaN(std::ostream& log, std::string_view type_name)
{
std::string nl{
std::format("std::numeric_limits< {} >::", type_name)
};
log << std::format(
"========\n"
" {}\n"
"========\n"
"\n"
"IEEE 754 Support\n"
" {}{:17} : {}\n"
" {}{:17} : {}\n"
" {}{:17} : {}\n"
"\n"
, type_name
, nl, "is_iec559", std::numeric_limits<NumT>::is_iec559
, nl, "has_quiet_NaN", std::numeric_limits<NumT>::has_quiet_NaN
, nl, "has_signaling_NaN", std::numeric_limits<NumT>::has_signaling_NaN
);
test_NaN<NumT, bits_type>(
log,
nl + "quiet_NaN()",
std::numeric_limits<NumT>::quiet_NaN());
test_NaN<NumT, bits_type>(
log,
nl + "signaling_NaN()",
std::numeric_limits<NumT>::signaling_NaN());
log << "\n\n";
}
//------------------------------------------------------------------
}
int main() {
auto& log{ std::cout };
put_predefined_macros(log);
test_NaN<float, std::uint32_t>(log, "float");
test_NaN<double, std::uint64_t>(log, "double");
}
// end file: app23.signaling_nan.main.cpp
This is a long standing bug in MSVC (since at least 2017, but I think there are earlier reports, e.g How to use std::signaling_nan?):
std::numeric_limits<float>::signaling_NaN()
is implemented via __builtin_nansf("1")
which seems to return the wrong value.