代码之家  ›  专栏  ›  技术社区  ›  jpo38

std::num_put由于从浮点自动转换为双精度导致nan装箱问题

  •  3
  • jpo38  · 技术社区  · 7 年前

    我正在使用 this post 使用一些额外的信息和 this post 修改 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 ,不适合 浮动 . 所以我的 双重的 ,丢失扩展信息。

    我知道还有一些选择,比如使用 FloatFormat 从第二篇文章,还是简单地写一篇聪明的文章 float2double 函数,并在将我的NaN值发送到输出流之前调用它,但它们要求开发人员处理这种情况……他可能忘记了。

    难道没有办法在内部实现这一点吗 类或任何其他可以在 浮动 被送到充满活力的人那里 stream 就像它能为一家公司工作一样好 ?

    我的要求是能够简单地调用如下函数 fixNaNToStream 标准::cout std::stringstream ,…)然后发送 和 双重的

    2 回复  |  直到 7 年前
        1
  •  1
  •   jpo38    7 年前

    考虑到这一点,一种非常简单的处理方法是将最右边的位用于浮点。对于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
    

    谢谢你,鲍勃!

        2
  •  1
  •   Bob__    7 年前

    问题是num_put只有double的虚拟do_put,而不是float。因此,我的浮动被悄悄地转换为双精度,丢失了扩展信息。

    信息丢失是因为当数字转换为数字时,携带信息的位的位置不同 float 到 double :

    // 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

    IEEE-754不要求复制NaN来保留其位表示(符号和有效载荷),尽管大多数实现都需要。

    在我以前回答这个问题的尝试中,我使用了一些不同偏移量的显式位移位来获得结果,但是 jpo38 还发现,最简单的方法是始终生成 浮动 然后正确地投下。

    std::nanf 可用于生成“定制的” 浮动

    #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);
    }