如何将双精度转换为字节以存储在 c++ 中?

How can a double be converted to bytes to be stored in c++?

提问人:Sam Moldenha 提问时间:8/14/2023 最后编辑:Sam Moldenha 更新时间:8/14/2023 访问量:123

问:

我正在尝试编写一个程序,该程序可以获取特定类型的列表,例如 or 并将其转换为字节,并将其写入可以转换回原始列表的文件。我想出了以下几个功能。它适用于列表,但是,如果它是列表,则不行,我试图了解原因。doublefloatstructfloatdoubles

template<typename T>
struct writer{
    static constexpr size_t Size = sizeof(T); 
    //the size to determing if it needs to be converted to uint16_t, uint32_t, etc...
    static constexpr bool U = std::conditional<std::is_unsigned_v<T>, std::true_type, 
          typename std::conditional<std::is_same_v<T, float>, std::true_type,
          typename std::conditional<std::is_same_v<T, double>, std::true_type, std::false_type>::type >::type >::type::value;
    //this is used to determine if the storing_num variable needs to be stored as unsigned or not
    using value_t = std::conditional_t<sizeof(T) == 1, 
          std::conditional_t<U, uint8_t, int8_t>,
          std::conditional_t<sizeof(T) == 2,
          std::conditional_t<U, uint16_t, int16_t>,
          std::conditional_t<sizeof(T) == 4,
          std::conditional_t<U, uint32_t, int32_t>, //by default the only options are 1, 2, 4, and 8
          std::conditional_t<U, uint64_t, int64_t> > > >;
    //the value that will either be entered or bit_casted to (shown in convert_num function)
    value_t storing_num;
    using my_byte = std::conditional_t<U == true, uint8_t, int8_t>;
    std::array<my_byte, Size> _arr;
    bool convert_num(T inp){
        static_assert(sizeof(T) == sizeof(value_t), "T and value_t need to be the same size");
        if constexpr (!std::is_same_v<T, value_t>){
            storing_num = std::bit_cast<value_t>(inp);
        }else{
            storing_num = inp;
        }

        auto begin = _arr.begin();
        for(int32_t i = _arr.size() - 1; i >= 0; --i, ++begin){
            *begin = ((storing_num >> (i << 3)) & 0xFF);
        }
        return true;
    }
    bool write(std::ostream& outfile){
        auto begin = _arr.cbegin();
        auto end = _arr.cend();
        for(;begin != end; ++begin)
            outfile << (char)(*begin);
        return true;
    }

};

以下内容可用于成功将 OR 写入文本文件。以下内容可用于读回其中一个数字:floatuint32_t

template<typename T>
struct reader{
    static constexpr size_t Size = sizeof(T);
    static constexpr bool U = std::conditional<std::is_unsigned_v<T>, std::true_type, 
          typename std::conditional<std::is_same_v<T, float>, std::true_type,
          typename std::conditional<std::is_same_v<T, double>, std::true_type, std::false_type>::type >::type >::type::value;
    using value_t = std::conditional_t<sizeof(T) == 1, 
          std::conditional_t<U, uint8_t, int8_t>,
          std::conditional_t<sizeof(T) == 2,
          std::conditional_t<U, uint16_t, int16_t>,
          std::conditional_t<sizeof(T) == 4,
          std::conditional_t<U, uint32_t, int32_t>, //by default the only options are 1, 2, 4, and 8
          std::conditional_t<U, uint64_t, int64_t> > > >;
    value_t outp;
    std::array<int8_t, Size> _arr;
    bool add_nums(std::ifstream& in){
        static_assert(sizeof(T) == sizeof(value_t), "T and value_t need to be the same size");
        _arr[0] = in.get();
        if(_arr[0] == -1)
            return false;
        for(uint32_t i = 1; i < _arr.size(); ++i){
            _arr[i] = in.get();
        }
        return true;
    }
    bool convert(){
        if(std::any_of(_arr.cbegin(), _arr.cend(), [](int v){return v == -1;}))
            return false;
        outp = 0;
        if(U){
            auto begin = _arr.cbegin();
            for(int32_t i = _arr.size()-1; i >= 0; i--, ++begin){
                outp += ((uint8_t)(*begin) << (i * 8));
            }
            return true;
        }
        auto begin = _arr.cbegin();
        for(int32_t i = _arr.size() - 1; i >= 0; --i, ++begin)
            outp += ((*begin) << (i << 3));
        return true;
    }
};

然后,我使用以下函数遍历文本文件并读/写该文件:

template<typename T>
void read_list(T* begin, const char* filename){
    reader<T> my_reader;
    std::ifstream in(filename);
    if(in.is_open()){
        while(in.good()){
            if(!my_reader.add_nums(in))
                break;
            if(!my_reader.convert()){
                std::cerr << "error reading, got -1 from num reading " << filename;
                return;
            }
            if(std::is_same_v<T, typename reader<T>::value_t >) *begin = my_reader.outp;
            else *begin = std::bit_cast<T>(my_reader.outp);
            ++begin;

        }
    }
    if(!in.eof() && in.fail()){
        std::cerr << "error reading " << filename;
        return;
    }
    in.close();
    return; 
}

template<typename T>
void write_list(T* begin, T* end, const char* filename){
    writer<T> my_writer;
    std::ofstream outfile(filename, std::ios::out | std::ios::binary | std::ios::trunc);
    for(;begin != end; ++begin){
        my_writer.convert_num(*begin);
        my_writer.write(outfile);
    }
}

例如,以下操作将按预期工作:

void write_float_vector(){
    std::vector<float> my_floats = {4.981, 832.991, 33.5, 889.56, 99.8191232, 88.192};
    std::cout<<"my_floats: " << my_floats<<std::endl;
    write_list(&my_floats[0], &my_floats[my_floats.size()], "binary_save/float_try.nt");
}

void read_floats(){
    std::vector<float> my_floats(6);
    read_list(&my_floats[0], "binary_save/float_try.nt");
    std::cout<<"my_floats: " << my_floats<<std::endl;
}

int main(){
    write_double_vector();
    std::cout<<"reading..."<<std::endl;
    read_doubles();
}

但是,如果它转换为 s 而不是 s,则无法正确读回双精度值。为什么双打会失败?doublefloat

例如,根据函数输出的内容,以下操作将失败:read_doubles

void write_double_vector(){
    std::vector<double> my_doubles = {4.981, 832.991, 33.5, 889.56, 99.8191232, 88.192};
    std::cout<<"my_doubles: " << my_doubles<<std::endl;
    write_list(&my_doubles[0], &my_doubles[my_doubles.size()], "binary_save/double_try.nt");
}

void read_doubles(){
    std::vector<double> my_doubles(6);
    read_list(&my_doubles[0], "binary_save/double_try.nt");
    std::cout<<"my_doubles: " << my_doubles<<std::endl;
}

附加

如果您想自己运行代码,我添加了这些帮助程序函数,并使用以下标头使其更易于重现:

#include <cstddef>
#include <cstdint>
#include <ios>
#include <stdio.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <array>
#include <bit>


template<typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& v){
    os << "{";
    for(uint32_t i = 0; i < v.size()-1; ++i)
        os << v[i]<<',';
    os << v.back() << "}";
    return os;
}

++ C++20 uint8t uint64

评论

0赞 Ted Lyngmo 8/14/2023
当然可以,但它不是“文本文件”。
0赞 Ted Lyngmo 8/14/2023
所有的标题是什么样的?他们提供了哪些尚未提供的内容?<sys/_types/_int16_t.h><cstdint>
0赞 Sam Moldenha 8/14/2023
@TedLyngmo我认为是这样,老实说,我使用 nvim 作为我的 IDE,当我在编码时使用这些类型时,它会自动添加这些标头,我几乎只是复制和粘贴。删除它。
1赞 Jarod42 8/14/2023
顺便说一句,不确定/表示是否固定。floatdouble
1赞 paddy 8/14/2023
对于任何想要尝试理解这些代码片段的人,我将其调整为一个不依赖于编写文件的最小可重现示例godbolt.org/z/esEE8c13G

答:

4赞 Ted Lyngmo 8/14/2023 #1

逐个字符构建可能会导致陷阱表示,因此不要这样做。将定义替换为 ,然后使用 + :floatdouble_arrstd::array<char, Size> _arr;in.readstd::memcpy

例:

#include <cstring> // std::memcpy
// writer:
std::array<char, Size> _arr;

bool convert_num(T inp) {
    static_assert(sizeof(T) == sizeof(value_t),
                    "T and value_t need to be the same size");
    std::memcpy(_arr.data(), &inp, Size);
    return true;
}

bool write(std::ostream& outfile) {
    return static_cast<bool>(outfile.write(_arr.data(), Size));
}
// reader:
std::array<char, Size> _arr;

bool add_nums(std::istream& in) {
    return static_cast<bool>(in.read(_arr.data(), Size));
}

bool convert() {
    std::memcpy(&outp, _arr.data(), Size);
    return true;
}

演示(最初来自 Paddy)

评论

0赞 Sam Moldenha 8/14/2023
以下方法不适用于双精度,并使浮点大小写也不再起作用。
0赞 Ted Lyngmo 8/14/2023
@SamMoldenha 如果你用同样的方式写它们,它就会起作用。我把它添加到答案中。以上是我需要进行的唯一更改,以使 Paddys 测试用例正常工作。
0赞 paddy 8/14/2023
干净多了。但有一点是我注意到原始代码以相反的字节顺序存储数据。如果这是一个实际的需求,那么 memcpy 将无济于事。如果将其用于存储整数,则需要担心通常的跨平台字节序问题。
0赞 Ted Lyngmo 8/14/2023
@paddy 这是真的。对于字节性,在读取/写入之前,必须添加一个检查,以检查缓冲区是否要反转。最重要的是在填充 s(和 s)时使用。我过去见过这样做会导致各种问题。std::memcpydoublefloat
2赞 paddy 8/14/2023 #2

程序的主要问题是在读取时会移动较小的数据类型。你在两个地方都有这个(为了清楚起见,这里一起显示,尽管脱离了上下文):

outp += ((uint8_t)(*begin) << (i * 8));
outp += ((*begin) << (i << 3));

在这两种情况下,迭代器都引用基础类型 。这不仅是一个有符号类型,而且你信任编译器,以将类型提升为足够大的整数。几年前,我实际上遇到了类似的问题。beginint8_t

请参阅此处:移动和屏蔽 32 位值时未定义的高阶uint64_t

解决方法是在移位之前强制转换为正确的大小:

outp += (((value_t)(uint8_t)*begin) << (i * 8));
outp += (((value_t)(uint8_t)*begin) << (i << 3));

我还想建议您避免使用像 代替 .根本没有必要尝试变得聪明。编译器会做正确的事情,而你只是让代码更难阅读。它还缺乏一致性,因为您在同一函数中使用两种形式。i << 3i * 8

评论

0赞 Jarod42 8/14/2023
演示
1赞 Red.Wave 8/14/2023 #3

在现代(C++20)中唯一正确的(不是UB)和方法是:constexprstd::bit_cast

#include <bit>
#include <array>
#include <algorithm>


double x;
auto x_bytes = std::bit_cast<std::array<std::unit8_t, sizeof(x)>(x);

if (std::endian::native==std::endian::little)
    std::ranges::reverse(x_bytes);

auto y = bit_cast<double>(x_bytes);

C++ 标准已经消除了所有其他选项。他们可能在 sime 平台上工作;但作为UB,它们可能会在未指定的条件下破裂并导致意外的结果。 如果输入和输出大小相同且易于复制,则有效。其他选项包括使用严格的别名规则产生问题,这是由于程序员错误导致的错误来源,以及基于类型双关语,这是前两个选项中最差的。 还具有该属性,这意味着它可以用于需要在编译时计算的表达式(创建无类型模板参数或数组的元素计数......std::bit_castreinterpret_caststd::memcpyunionstd::bit_castconstexpr

评论

1赞 Ted Lyngmo 8/15/2023
我喜欢这个,所以 +1 虽然“消除所有其他选项”可能有点强。 仍然工作得很好。也许还应该有一个 just 来确保它不会在混合端环境中使用。memcpystatic_assert(std::endian::native==std::endian::big || std::endian::native==std::endian::little);
0赞 Red.Wave 8/15/2023
memcpy是 C API;很容易混合源和目标,或者获得导致 UB 的重叠 meemory 范围。编译器确实在幕后使用它,并提供编译时安全证明。但它在用户代码中并不好。C++ 的重点正在转向编译时评估。因此,无论目前有任何警告,都没有像这样的解决方案不是第一选择。我可以编辑帖子,但我需要看到除 或 以外的单个选项。您可以在 Google 上搜索 vs 以检查由于不负责任地使用后者而造成的损坏。constexprmemcpymemcpyunionreinterpreted_castmemmovememcpy
0赞 Red.Wave 8/15/2023
Endian 是一个旁注。通常,当通过辅助存储或网络传输字节和位时,我们需要执行嵌套到主机或主机到嵌套的转换。否则,不匹配的排序会导致问题。然而,我并不打算把重点放在字节序上。
0赞 Ted Lyngmo 8/15/2023
除了“消除”的部分之外,我同意所有内容,但这个小细节不值得更改(至少不是为了我)。