Od zeszłego tygodnia prawie codziennie prowadzę "wirtualne warsztaty dla początkujących z jakości kodu" - w dużym skrócie: analizuje i sugeruje rzeczy do poprawienia / zrobienia inaczej w kodzie, który otrzymuje od widzów. Wczorajszy (#6) odcinek był o dość krótkim i ciekawym kodzie w C++, który między innymi zapisywał ("manualnie") plik BMP. Ponieważ sam nagłówek BMP był tworzony/trzymany bezpośrednio w tablicy bajtów - co nie jest specjalnie czytelne - zasugerowałem użycie struktury. To z kolei spotkało się z zaskoczeniem jednego z widzów (w komentarzach pod wideo) z uwagi na dopełnienie (padding) / wyrównanie pól (alignment). Delikatnie rozbudowaną wersję mojej odpowiedzi zamieszczam poniżej.
Klasyczną metodą odczytu/zapisu nagłówków w C/C++ są/były zapisy/odczyty całych instancji struct'ów. A co z paddingiem? Ten się po prostu wyłącza dla danej struktury za pomocą jednego z dwóch sposobów:
Przykładowy kod:
#include <cstdio>
#include <cstdint>
struct __attribute__((__packed__)) SomeFileHeader {
uint8_t version;
uint16_t width;
uint16_t height;
uint8_t compression;
};
int main() {
SomeFileHeader header;
header.version = 2;
header.width = 123;
header.height = 78;
header.compression = 0;
FILE *f = fopen("out.bin", "wb");
if (f == nullptr) {
perror("Failed to create file:");
return 1;
}
fwrite(&header, 1, sizeof(header) /* 6 bytes */, f);
fclose(f);
}
Formalnie dowolny obiekt w C/C++ może zostać odczytany jako seria bajtów (unsigned char), oraz może zostać odtworzony z serii bajtów – standardy C/C++ zawierają porozrzucane informacje o tym – więc to nie jest problemem. Drobny druk dotyczy oczywiście wskaźników i czasu życia obiektów na które te wskazują, ale ten problem nie dotyczy nagłówków plików, które pointerów nie mają - co najwyżej offsety.
Powyższe podejście oczywiście nie działa w przypadku:
Natomiast na streamie nie chodziło mi o bezpośredni zapis struktury, tylko o wrzucenie wszystkich pól do struktury dla czytelności, a potem zapis pola po polu. Zazwyczaj tworzy się do tego zestaw metod/funkcji wspomagających typu write_int8, write_int16, write_int32, etc - takie patterny można znaleźć w wielu encoderach (lub analogiczne z "read" w wielu parserach).
Najlepiej byłoby opakować to w klasę typu "BMPInfoHeader", która oprócz pól tego nagłówka będzie miała również metodę typu "build", "bake", "make", "serialize", "toBinaryData" (zwał jak zwał), która zwróci (albo wypełni podany) vector<uint8_t> (lub ananalogiczną strukturę danych) na podstawie zawartości pól. Alternatywnie może mieć metodę "write", która dostanie wskaźnik do pliku i pozapisuje pola do niego (korzystając ze wspomnianych write_int8, ...) - tu by można się zastanowić czy to nie byłaby zbytnia specjalizacja klasy, ale to kwestia projektu.
Przykładowy kod:
#include <cstdio>
#include <cstdint>
#include <vector>
#include <cassert>
// Helper class (normally it would go to a different file).
// It's a bit simplified too.
class HelperBinaryWriter {
private:
std::vector<uint8_t> data_;
public:
const std::vector<uint8_t>& get_data() const {
return data_;
}
void write_uint8(uint8_t v) {
data_.push_back(v);
}
void write_uint16le(uint16_t v) {
write_uint8((uint8_t)v);
write_uint8((uint8_t)(v >> 8));
}
};
class SomeFileHeader {
public: // Or private + getters/setters if you prefer.
static const size_t HEADER_SIZE = 1 + 2 + 2 + 1;
uint8_t version;
uint16_t width;
uint16_t height;
uint8_t compression;
std::vector
Powyższy pomysł można by zaimplementować również na kilka innych sposobów, ale ostatecznie powinniśmy otrzymać czytelny kod, który przy okazji jest przenośny pomiędzy platformami/kompilatorami, a także całkiem łatwy do rozszerzenia.
Dodatkowe materiały: