我正在从格式上跨平台读取的文件中读取文件,但是根据文件所针对的平台,字节序的大小可能不同。所述平台由文件中的值定义。
当前,我处理字节序的方式是使用if语句,一个正常读取文件,另一个使用byteswap intrinsics:
// source.h
class File {
public:
enum class Endian {
Little = 1,
Big = 2
};
};
// ...removed...
// source.cpp
#include "source.h"
#include <fstream>
std::ifstream file;
File::Endian endianness;
// ...removed...
bool GetPlatform() {
uint32_t platform;
file.read(reinterpret_cast<char*>(&platform), sizeof(platform));
if (platform == 1) {
endianness = File::Endian::Little;
}
else if (platform == 2 << 24) {
endianness = File::Endian::Big;
}
// ...removed...
}
void ReadData() {
uint32_t data;
uint32_t dataLittle;
if (endianness == File::Endian::Little) {
file.read(reinterpret_cast<char*>(&data), sizeof(data));
}
else if (endianness == File::Endian::Big) {
file.read(reinterpret_cast<char*>(&data), sizeof(data));
dataLittle = _byteswap_ulong(data);
}
}
我的问题是,有可能在大字节序时放弃每个值的交换,而是普遍设置字节序吗?以下是我的意思的潜在示例:
bool GetPlatform() {
uint32_t platform;
file.read(reinterpret_cast<char*>(&platform), sizeof(platform));
if (platform == 1) {
// Universally set the endianness to little endian
}
else if (platform == 2 << 24) {
// Universally set the endianness to big endian
}
// ...removed...
}
void ReadData() {
uint32_t data;
file.read(reinterpret_cast<char*>(&data), sizeof(data)); // Data is now read correctly regardless of endianness
}
我问这个问题的主要原因是,基本上,每个函数的代码量减少了一半,因为if语句不再需要字节序。
此外,std::endian是否可用于此任务?其示例仅指示用于检测主机字节顺序,但我不确定它是否还有其他用途。
我认为通常的答案是#ifdef像read64这样的函数的定义:
int64_t read64(char *pos) {
#ifdef IS_BIG_ENDIAN
...
#elif IS_LITTLE_ENDIAN
...
#else
// probably # error
#endif
}
以正确的字节序“自动”读取的唯一方法是使CPU的本地字节序与文件中字节的字节序匹配。如果它们不匹配,那么您的代码中的某些内容需要知道从文件的字节序到CPU的字节序(从文件中读取时),反之亦然(写入文件时),进行必要的字节交换。 )。
[检查如何实现ntohl()和htonl()以处理网络顺序(又名big-endian)数据-在big-endian平台(如PowerPC)上,它们是简单的no-ops,逐字返回其参数。在小端平台上(例如Intel),它们返回参数字节交换。这样,调用它们的代码不必在运行时进行任何条件测试即可确定字节交换是否合适,它只是无条件地运行通过ntohl()
或ntohs()
读取的所有数据。 ],并相信他们会在所有平台上对数据进行正确的处理。同样,在写入数据时,它会无条件地通过htonl()
或htons()
运行所有数据值,然后再将数据发送到文件/网络/任何地方。
您的程序可以通过调用那些实际函数,或者(如果您需要读取的数据类型不只是16位和/或32位整数,则可以通过查找或编写类似的函数来)执行相似的操作。对于那些精神上的人,例如类似于:
inline uint32_t NativeToLittleEndianUint32(uint32_t val) {...}
inline uint32_t LittleEndianToNativeUint32(uint32_t val) {...}
inline uint32_t NativeToBigEndianUint32(uint32_t val) {...}
inline uint32_t BigEndianToNativeUint32(uint32_t val) {...}
[...]
inline uint64_t NativeToLittleEndianUint64(uint64_t val) {...}
inline uint64_t NativeToBigEndianUint64(uint64_t val) {...}
inline uint64_t LittleEndianToNativeUint64(uint64_t val) {...}
inline uint64_t BigEndianToNativeUint64(uint64_t val) {...}
[...]
...等。代码中成千上万的if / then子句消失了,取而代之的是编译时条件逻辑。这使得代码更高效,更易于测试,并且不易出错。如果您喜欢模板化函数,则可以使用它们来减少调用代码编写者需要记住的函数名的数量(例如,可以让inline template<T> NativeToLittleEndian(T val) {...}
与模板重写一起对所需的所有类型进行正确的处理)支持)
如果想进一步介绍,可以将读/写和字节交换功能组合在一起,成为一个较大的函数,这样就不必为每个数据值进行两次函数调用。
注:为浮点类型实现这些功能时要小心;一些CPU架构(例如Intel)会隐式修改意外的浮点位模式,这意味着在字节序交换32位浮点值时,需要将该值的非本机/外部/字节交换表示形式存储为uint32_t而不是“ float”。如果您想在我的代码中看到如何处理此问题的示例,请查看例如B_HOST_TO_BENDIAN_IFLOAT
中B_BENDIAN_TO_HOST_IFLOAT
和this file宏的定义。
如果我了解您的情况,您的基本问题是您缺乏某种抽象水平。您有一堆函数,可以从文件中读取各种数据结构。由于这些函数直接调用std::ifstream::read
,因此它们都需要知道它们正在读取的结构和文件的布局。那是两个任务,这比理想的任务还要多。您最好将此逻辑分为两个抽象级别。我们将它们称为新级别ReadBytes
的函数,因为它们专注于从文件中获取字节。由于Microsoft提供了三个bytewap内部函数,因此将有三个功能。这是第一个刺破4字节值的方法。
void ReadBytes(std::ifstream & file, File::Endian endianness, uint32_t & data) {
file.read(reinterpret_cast<char*>(&data), sizeof(data));
if (endianness == File::Endian::Big) {
data = _byteswap_ulong(data);
}
}
注意,我已经通过参数返回了数据。这是为了允许所有三个功能具有相同的名称。此参数的类型告诉编译器要使用哪个重载。 (还有其他方法。编码样式不同。)
还有其他改进,但这足以创建新的抽象级别。从文件读取数据的各种功能将更改为如下所示。
void ReadData() {
uint32_t data;
ReadBytes(file, endianness, data);
// More processing here, maybe more reads.
}
使用这种小的示例代码,节省起来并不容易。但是,您指出可能有许多功能可以充当ReadData
的角色。这种方法将校正字节序的负担从那些功能转移到了新的ReadBytes
功能上。 if
语句的数量从“数百个(如果不是数千个)减少到三个”。
此更改是由通常称为“不要重复自己”的编程原理所推动的。相同的原则会引发诸如“为什么需要此代码的功能不止一个的问题?”
另一个使您感到麻烦的问题是,您似乎对问题采取了一种程序化的方法,而不是面向对象的方法。程序方法的症状可能包括过多的函数参数(例如endianness
作为参数)和全局变量。如果将接口包装在一个类中,它将更易于使用。这是start,用于声明这样的类(即,头文件的开始)。请注意,字节序是私有的,并且此标头没有指示如何确定字节序。如果您具有良好的封装,则此类之外的代码将不会在意哪个平台创建了文件。
// Designed as a drop-in replacement for an ifstream.
// (Non-public inheritance *might* be appropriate if you want to restrict the interface.)
class IFile : public std::ifstream {
private:
File::Endian endianness;
public:
// Mimic the constructors of std::ifstream that you need.
explicit IFile(const std::string & filename);
// It should be possible to use some template magic to simplify the
// definition of these three functions, but since there are only three:
void ReadBytes(uint16_t & data) {
file.read(reinterpret_cast<char*>(&data), sizeof(data));
if (endianness == File::Endian::Big) {
data = _byteswap_ushort(data);
}
}
void ReadBytes(uint32_t & data) {
file.read(reinterpret_cast<char*>(&data), sizeof(data));
if (endianness == File::Endian::Big) {
data = _byteswap_ulong(data);
}
}
void ReadBytes(uint64_t & data) {
file.read(reinterpret_cast<char*>(&data), sizeof(data));
if (endianness == File::Endian::Big) {
data = _byteswap_uint64(data);
}
}
};
这只是开始。一方面,界面需要做更多的工作。此外,可以更方便地编写ReadBytes
函数,也许可以使用std::endian
代替小端。 (Boost有一个endian library,可以帮助您制作真正的可移植代码。它甚至默认情况下在可用时使用内在函数。)
字节序的确定是在实现(源)文件中完成的。似乎应该在打开文件的过程中完成此操作。在本示例中,我已将其作为构造函数的一部分,但您可能需要更大的灵活性(使用ifstream
接口作为准则)。无论如何,在该类的实现之外都不需要访问用于检测平台的逻辑。这是实施的开始。
// Helper function, not needed outside this class.
// This should be either static or put into an anonymous namespace.
static File::Endian ReadEndian(std::ifstream & file) {
uint32_t platform;
file.read(reinterpret_cast<char*>(&platform), sizeof(platform));
if (platform == 1) {
return File::Endian::Little;
}
else if (platform == 2 << 24) {
return File::Endian::Big;
}
// Handle unrecognized platform here
}
IFile::IFile(const std::string & filename) : std::ifstream(filename),
endianness(ReadEndian(file))
{}
此时,您的各种ReadData
函数可能类似于以下内容(不使用全局变量)。
void ReadData(IFile & file) {
uint32_t data;
file.ReadBytes(data);
}
这比您想要的要简单,因为重复的代码更少。 (铸造到char*
并获取尺寸不再需要在任何地方重复。)
总结,有两个主要方面需要改进。
两者都有助于简化安全更改等新操作,例如支持新的字节序。没有用于设置字节序的预先构建的开关,但是当您的代码组织得更好时,构建它并不难。