Если я понимаю вашу ситуацию, ваша основная проблема c в том, что вам не хватает уровня абстракции. У вас есть несколько функций, которые читают различные структуры данных из вашего файла. Поскольку эти функции напрямую вызывают std::ifstream::read
, им всем необходимо знать как структуру, которую они читают, так и структуру файла. Это две задачи, что более чем идеально. Вам лучше разбить эту логику c на два уровня абстракции. Давайте назовем функции для нового уровня ReadBytes
, поскольку они сосредоточены на получении байтов из файла. Поскольку Microsoft предоставляет три встроенных байтовых замены, таких функций будет три. Вот первый удар по одному для 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
в качестве параметра) и глобальные переменные. Интерфейс будет проще в использовании, если он будет заключен в класс. Вот начало в направлении объявления такого класса (т.е. начало файла заголовка). Обратите внимание, что порядковый номер является частным, и этот заголовок не имеет указаний на то, как определяется порядковый номер. Если у вас хорошая инкапсуляция, код вне этого класса не будет заботиться о том, какая платформа создала файл.
// 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
вместо предположения little-endian. (Boost имеет библиотеку с порядком байтов , которая может помочь вам создать действительно переносимый код. Он даже по умолчанию использует встроенные функции, когда они доступны.)
Определение порядка следования выполняется в реализации (источник ) файл. Кажется, что это должно быть сделано как часть открытия файла. Я поместил это как часть конструктора для этого примера, но вам может потребоваться больше гибкости (используйте интерфейс ifstream
в качестве руководства). В любом случае, логика c для обнаружения платформы не должна быть доступна вне реализации этого класса. Вот начало реализации.
// 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*
и получение размера больше не нужно повторять повсюду.)
Таким образом, есть две основные области для улучшения.
- Не повторяйте себя. Код, который часто повторяется, следует перенести в отдельную функцию.
- Объектно-ориентированный. Полагайтесь на объекты, обрабатывающие рутинные задачи, а не делегируя их людям, использующим объекты.
Оба из них способствуют облегчению безопасного внесения радикальных изменений, таких как поддержка нового порядка байтов. Нет заранее встроенного переключателя для установки порядка байтов, но его не так сложно создать, когда ваш код лучше организован.