std :: num_put проблема с нанобоксом из-за автоматического приведения из float в double - PullRequest
0 голосов
/ 11 декабря 2018

Я использую этот пост , чтобы расширить значения nan с некоторой дополнительной информацией, и этот пост , чтобы изменить поведение std::cout и отобразить эту дополнительную информацию.

Вот код, определяющий функции и NumPut класс:

#include <iostream>
#include <assert.h>
#include <limits>
#include <bitset>
#include <cmath>
#include <locale>
#include <ostream>
#include <sstream>

template <typename T>
void showValue( T val, const std::string& what )
{
    union uT {
      T d;
      unsigned long long u;
    };
    uT ud;
    ud.d = val;
    std::bitset<sizeof(T) * 8> b(ud.u);
    std::cout << val << " (" << what << "): " << b.to_string() << std::endl;
}

template <typename T>
T customizeNaN( T value, char mask )
{
    T res = value;
    char* ptr = (char*) &res;
    assert( ptr[0] == 0 );
    ptr[0] |= mask;
    return res;
}

template <typename T>
bool isCustomNaN( T value, char mask )
{
    char* ptr = (char*) &value;
    return ptr[0] == mask;
}

template <typename T>
char getCustomNaNMask( T value )
{
    char* ptr = (char*) &value;
    return ptr[0];
}

template <typename Iterator = std::ostreambuf_iterator<char> >
class NumPut : public std::num_put<char, Iterator>
{
private:
    using base_type = std::num_put<char, Iterator>;

public:
    using char_type = typename base_type::char_type;
    using iter_type = typename base_type::iter_type;

    NumPut(std::size_t refs = 0)
    :   base_type(refs)
    {}

protected:
    virtual iter_type do_put(iter_type out, std::ios_base& str, char_type fill, double v) const override {
        if(std::isnan(v))
        {
            char mask = getCustomNaNMask(v);
            if ( mask == 0x00 )
            {
                out = std::copy(std::begin(NotANumber), std::end(NotANumber), out);
            }
            else
            {
                std::stringstream maskStr;
                maskStr << "(0x" << std::hex << (unsigned) mask << ")";
                std::string temp = maskStr.str();
                out = std::copy(std::begin(CustomNotANumber), std::end(CustomNotANumber), out);
                out = std::copy(std::begin(temp), std::end(temp), out);
            }
        }
        else
        {
            out = base_type::do_put(out, str, fill, v);
        }
        return out;
    }

private:
    static const std::string NotANumber;
    static const std::string CustomNotANumber;
};

template<typename Iterator> const std::string NumPut<Iterator>::NotANumber = "Not a Number";
template<typename Iterator> const std::string NumPut<Iterator>::CustomNotANumber = "Custom Not a Number";

inline void fixNaNToStream( std::ostream& str )
{
    str.imbue( std::locale(str.getloc(), new NumPut<std::ostreambuf_iterator<char>>() ) );
}

Простая тестовая функция:

template<typename T>
void doTest()
{
    T regular_nan = std::numeric_limits<T>::quiet_NaN();
    T myNaN1 = customizeNaN( regular_nan, 0x01 );
    T myNaN2 = customizeNaN( regular_nan, 0x02 );

    showValue( regular_nan, "regular" );
    showValue( myNaN1, "custom 1" );
    showValue( myNaN2, "custom 2" );
}

Моя основная программа:

int main(int argc, char *argv[])
{
    fixNaNToStream( std::cout );

    doTest<double>();
    doTest<float>();

    return 0;
}

doTest<double> Выходы:

Not a Number (regular): 0111111111111000000000000000000000000000000000000000000000000000
Custom Not a Number(0x1) (custom 1): 0111111111111000000000000000000000000000000000000000000000000001
Custom Not a Number(0x2) (custom 2): 0111111111111000000000000000000000000000000000000000000000000010

doTest<float> Выходы:

Not a Number (regular): 01111111110000000000000000000000
Not a Number (custom 1): 01111111110000000000000000000001
Not a Number (custom 2): 01111111110000000000000000000010

В то время как я ожидал бы для float:

Not a Number (regular): 01111111110000000000000000000000
Custom Not a Number(0x1) (custom 1): 01111111110000000000000000000001
Custom Not a Number(0x2) (custom 2): 01111111110000000000000000000010

Проблема в том, что num_put имеет только виртуальный do_put для double,не для float.Так что мой float молча преобразуется в double, теряя мою расширенную информацию.

Я знаю, что есть несколько альтернатив, например, использование FloatFormat из второго поста или просто написание умного float2doubleфункции и вызова его до отправки моего значения NaN в выходной поток, но они требуют, чтобы разработчик позаботился об этой ситуации ... и он может забыть.

Нет ли способа реализовать это в *Класс 1054 * или что-то еще, что просто заставило бы вещи работать, когда float отправляется на вставленный stream так же хорошо, как это работает для double?

Мое требование - иметь возможность простовызовите функцию типа fixNaNToStream для любого выходного потока (std::cout, локальный std::stringstream, ...), а затем отправьте ей float и double, чтобы они идентифицировались как мои собственные NaN и отображались соответствующим образом.

Ответы [ 2 ]

0 голосов
/ 14 декабря 2018

Как указал Боб, двойной расширенный бит должен находиться в той же относительной позиции, что и для смещенной экспоненты, чем для поплавка, если вы хотите, чтобы приведение работало в обоих направлениях (от плавающего к двойному и от двойного к плавающему).

Учитывая это, очень тривиальный подход к обработке, который заключается в использовании крайнего правого бита для числа с плавающей точкой.Поскольку для double вместо того, чтобы пытаться вручную определить, какой бит следует использовать, просто остановите операции приведения и позвольте системе определить, где находится правильное место ...

Тогда код становится:

#include <iostream>
#include <assert.h>
#include <limits>
#include <bitset>
#include <cmath>
#include <locale>
#include <ostream>
#include <sstream>

template <typename T>
void showValue( T val, const std::string& what )
{
    union uT {
      T d;
      unsigned long long u;
    };
    uT ud;
    ud.d = val;
    std::bitset<sizeof(T) * 8> b(ud.u);
    std::cout << val << " (" << what << "): " << b.to_string() << std::endl;
}

char& getCustomNaNMask( float& value )
{
    char* ptr = (char*) &value;
    return ptr[0];
}

/** temp parameter is mainly used because we can't have two functions with same prototype even if they return different values */
float getCustomizedNaN( char mask, float temp )
{
    // let's reuse temp argument as we need a local float variable
    temp = std::numeric_limits<float>::quiet_NaN();
    getCustomNaNMask(temp) |= mask;
    return temp;
}

/** temp parameter is mainly used because we can't have two functions with same prototype even if they return different values */
double getCustomizedNaN( char mask, double temp )
{
    float asFloat = getCustomizedNaN( mask, float() );
    // Let the system correctly cast from float to double, that's it!
    return static_cast<double>( asFloat );
}

template <typename T>
bool isCustomNaN( T value, char mask )
{
    return getCustomNaNMask(value) == mask;
}

template <typename Iterator = std::ostreambuf_iterator<char> >
class NumPut : public std::num_put<char, Iterator>
{
private:
    using base_type = std::num_put<char, Iterator>;

public:
    using char_type = typename base_type::char_type;
    using iter_type = typename base_type::iter_type;

    NumPut(std::size_t refs = 0)
    :   base_type(refs)
    {}

protected:
    virtual iter_type do_put(iter_type out, std::ios_base& str, char_type fill, double v) const override {
        if(std::isnan(v))
        {
            float asFloat = static_cast<float>( v );
            char& mask = getCustomNaNMask(asFloat);
            if ( mask == 0x00 )
            {
                out = std::copy(std::begin(NotANumber), std::end(NotANumber), out);
            }
            else
            {
                std::stringstream maskStr;
                maskStr << "(0x" << std::hex << (unsigned) mask << ")";
                std::string temp = maskStr.str();
                out = std::copy(std::begin(CustomNotANumber), std::end(CustomNotANumber), out);
                out = std::copy(std::begin(temp), std::end(temp), out);
            }
        }
        else
        {
            out = base_type::do_put(out, str, fill, v);
        }
        return out;
    }

private:
    static const std::string NotANumber;
    static const std::string CustomNotANumber;
};

template<typename Iterator> const std::string NumPut<Iterator>::NotANumber = "Not a Number";
template<typename Iterator> const std::string NumPut<Iterator>::CustomNotANumber = "Custom Not a Number";

inline void fixNaNToStream( std::ostream& str )
{
    str.imbue( std::locale(str.getloc(), new NumPut<std::ostreambuf_iterator<char>>() ) );
}

И тестовая программа:

template<typename T>
void doTest()
{
    T regular_nan = std::numeric_limits<T>::quiet_NaN();
    T myNaN1 = getCustomizedNaN( 0x01, T() );
    T myNaN2 = getCustomizedNaN( 0x02, T() );

    showValue( regular_nan, "regular" );
    showValue( myNaN1, "custom 1" );
    showValue( myNaN2, "custom 2" );
}

int main(int argc, char *argv[])
{
    fixNaNToStream( std::cout );

    doTest<double>();
    doTest<float>();

    return 0;
}

Выходы:

Not a Number (regular): 0111111111111000000000000000000000000000000000000000000000000000
Custom Not a Number(0x1) (custom 1): 0111111111111000000000000000000000100000000000000000000000000000
Custom Not a Number(0x2) (custom 2): 0111111111111000000000000000000001000000000000000000000000000000
Not a Number (regular): 01111111110000000000000000000000
Custom Not a Number(0x1) (custom 1): 01111111110000000000000000000001
Custom Not a Number(0x2) (custom 2): 01111111110000000000000000000010

Спасибо, Боб!

0 голосов
/ 13 декабря 2018

Проблема в том, что num_put имеет только виртуальный do_put для double, а не для float.Таким образом, мое число с плавающей точкой молча преобразуется в двойное число, теряя мою расширенную информацию.

Информация теряется, потому что позиции битов, ее переносящих, отличаются, когда число преобразуется из float в * 1006.*:

// Assuming an IEE-754 floating-point representation of float and double
0 11111111 10000000000000000000010
0 11111111111 1000000000000000000001000000000000000000000000000000

Обратите внимание, что биты мантиссы "сдвинуты" на 3 позиции, поскольку экспоненте требуется еще 3 бита.

Также стоит отметить, что указано на этой странице:https://en.cppreference.com/w/cpp/numeric/math/isnan

Копирование NaN не требуется, согласно IEEE-754, для сохранения его битового представления (знак и полезная нагрузка), хотя в большинстве реализаций это делается.

Я предполагаю, что то же самое верно для приведения таких значений, так что даже игнорирование других причин неопределенного поведения в коде OP, может ли метод NaN-бокса работать или нет, фактически определяется реализацией.

В моих предыдущих попытках ответить на этот вопрос я использовал некоторое явное смещение битов на другое смещение для достижения результата, но, как выяснилось jpo38 , самый простой способ - всегда генерировать float NaN и затем приведение правильно.

Функция стандартной библиотеки std :: nanf может быть использована для генерации "настроенного" float NaN, но в следующем демонстрационном фрагменте я выигралНе используйте его.

#include <cstdint>
#include <limits>
#include <cstring>
#include <cassert>
#include <type_traits>
#include <iostream>
#include <bitset>
#include <array>
#include <climits>

namespace my {

// Waiting for C++20 std::bit_cast
// source: https://en.cppreference.com/w/cpp/numeric/bit_cast
template <class To, class From>
typename std::enable_if<
    (sizeof(To) == sizeof(From)) &&
    std::is_trivially_copyable<From>::value &&
    std::is_trivial<To>::value,
    // this implementation requires that To is trivially default constructible
    To>::type
// constexpr support needs compiler magic
bit_cast(const From &src) noexcept
{
    To dst;
    std::memcpy(&dst, &src, sizeof(To));
    return dst;
}

template <typename T, std::size_t Size = sizeof(T)>
void print_bits(T x)
{
    std::array<unsigned char, Size> buf;
    std::memcpy(buf.data(), &x, Size);
    for (auto it = buf.crbegin(); it != buf.crend(); ++it)
    {
        std::bitset<CHAR_BIT> b{*it};
        std::cout << b.to_string();
    }
    std::cout << '\n';
}

// The following assumes that both floats and doubles store the mantissa
// in the lower bits and that while casting a NaN (float->double or double->float)
// the most significant of those aren't changed
template <typename T>
auto boxed_nan(uint8_t data = 0) -> typename std::enable_if<std::numeric_limits<T>::has_quiet_NaN, T>::type
{
    return bit_cast<float>(
        bit_cast<uint32_t>(std::numeric_limits<float>::quiet_NaN()) |
        static_cast<uint32_t>(data)
    );
}

template <typename T>
uint8_t unbox_nan(T num)
{
    return bit_cast<uint32_t>(static_cast<float>(num));
}

}; // End of namespace 'my'


int main()
{
    auto my_nan = my::boxed_nan<float>(42);
    my::print_bits(my_nan);
    my::print_bits(static_cast<double>(my_nan));
    assert(my::unbox_nan(my_nan) == 42);
    assert(my::unbox_nan(static_cast<double>(my_nan)) == 42);

    auto my_d_nan = my::boxed_nan<double>(17);
    my::print_bits(my_d_nan);
    my::print_bits(static_cast<float>(my_d_nan));
    assert(my::unbox_nan(my_d_nan) == 17);
    assert(my::unbox_nan(static_cast<float>(my_d_nan)) == 17);

    auto my_ld_nan = my::boxed_nan<long double>(9);
    assert(my::unbox_nan(my_ld_nan) == 9);
    assert(my::unbox_nan(static_cast<double>(my_ld_nan)) == 9);
}
...