How to mimic C#’s BinaryReader / BinaryWriter in C++
Writing primitive types and structs into disk
I was looking for a way to write floats/ints/strings
to a file, then read them as floats/ints/strings
. I needed it because of the 2D Game Editors I built. I wanted to be able to generate a character or map file, that can be read, updated, saved, etc.On top of the primitive types, I needed to also write a whole struct
and be able to read it in 1 line of code as a struct
(not have to read each property of the struct 1 by 1). That way the Game Engine can load the characters/maps and get the game rolling!Basically read and write as ios::binary
. I couldn’t find anything that does that for C++
. So I ended up writing it myself, thought I would share it :)
A couple of quick notes:
You can ignore the “BM” part, it was just a prefix for my game editor. I like to prefix file names to that I can type “BM…” and will see every single class that I wrote, without having to refer to the namespace.
The
BMLogging
is just a helper class that logs strings as needed.
#include <iostream> #include <fstream> using namespace std; namespace BMGameEngine { enum BMFileMode { Open = 0, Create, Append }; enum BMBinaryIOMode { BMBinaryIOMode_None = 0, BMBinaryIOMode_Read, BMBinaryIOMode_Write }; class BMBinaryIO { // the output file stream to write onto a file ofstream writer; // the input file stream to read from a file ifstream reader; // the filepath of the file we're working with string filePath; // the current active mode. BMBinaryIOMode currentMode; public: BMBinaryIO() { currentMode = BMBinaryIOMode_None; } // the destructor will be responsible for checking if we forgot to close // the file ~BMBinaryIO() { if(writer.is_open()) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "You forgot to call close() after finishing with the file! Closing it..."); writer.close(); } if(reader.is_open()) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "You forgot to call close() after finishing with the file! Closing it..."); reader.close(); } } // opens a file with either read or write mode. Returns whether // the open operation was successful bool open(string fileFullPath, BMBinaryIOMode mode) { filePath = fileFullPath; BMLOG_INFO(BMLoggingClass_BinaryIO, "Opening file: " + filePath); // Write mode if(mode == BMBinaryIOMode_Write) { currentMode = mode; // check if we had a previously opened file to close it if(writer.is_open()) writer.close(); writer.open(filePath.c_str(), ios::binary); if(!writer.is_open()) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "Could not open file for write: " + filePath); currentMode = BMBinaryIOMode_None; } } // Read mode else if(mode == BMBinaryIOMode_Read) { currentMode = mode; // check if we had a previously opened file to close it if(reader.is_open()) reader.close(); reader.open(filePath.c_str(), ios::binary); if(!reader.is_open()) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "Could not open file for read: " + filePath); currentMode = BMBinaryIOMode_None; } } // if the mode is still the NONE/initial one -> we failed return currentMode == BMBinaryIOMode_None ? false : true; } // closes the file void close() { if(currentMode == BMBinaryIOMode_Write) { writer.close(); } else if(currentMode == BMBinaryIOMode_Read) { reader.close(); } } // checks whether we're allowed to write or not. bool checkWritabilityStatus() { if(currentMode != BMBinaryIOMode_Write) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "Trying to write with a non Writable mode!"); return false; } return true; } // helper to check if we're allowed to read bool checkReadabilityStatus() { if(currentMode != BMBinaryIOMode_Read) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "Trying to read with a non Readable mode!"); return false; } // check if we hit the end of the file. if(reader.eof()) { BMLOG_ERROR(BMLoggingClass_BinaryIO, "Trying to read but reached the end of file!"); reader.close(); currentMode = BMBinaryIOMode_None; return false; } return true; } // so we can check if we hit the end of the file bool eof() { return reader.eof(); } // Generic write method that will write any value to a file (except a string, // for strings use writeString instead) template<typename T> void write(T &value) { if(!checkWritabilityStatus()) return; // write the value to the file. writer.write((const char *)&value, sizeof(value)); } // Writes a string to the file void writeString(string str) { if(!checkWritabilityStatus()) return; // first add a \0 at the end of the string so we can detect // the end of string when reading it str += '\0'; // create char pointer from string. char* text = (char *)(str.c_str()); // find the length of the string. unsigned long size = str.size(); // write the whole string including the null. writer.write((const char *)text, size); } // reads any type of value except strings. template<typename T> T read() { checkReadabilityStatus(); T value; reader.read((char *)&value, sizeof(value)); return value; } // reads any type of value except strings. template<typename T> void read(T &value) { if(checkReadabilityStatus()) { reader.read((char *)&value, sizeof(value)); } } // read a string value string readString() { if(checkReadabilityStatus()) { char c; string result = ""; while(!reader.eof() && (c = read<char>()) != '\0') { result += c; } #ifdef BMSYSTEM_DEBUG_MODE //BMLOG_INFO(BMLoggingClass::BinaryIO, "string value read: " + result); #endif return result; } return ""; } // read a string value void readString(string &result) { if(checkReadabilityStatus()) { char c; result = ""; while(!reader.eof() && (c = read<char>()) != '\0') { result += c; } #ifdef BMSYSTEM_DEBUG_MODE //BMLOG_INFO(BMLoggingClass::BinaryIO, "string value read: " + result); #endif } } }; }
Here’s how you would use it to WRITE:
string myPath = "somepath to the file"; BMBinaryIO binaryIO; if(binaryIO.open(myPath, BMBinaryIOMode::Write)) { float value = 165; binaryIO.write(value); char valueC = 'K'; binaryIO.write(valueC); double valueD = 1231.99; binaryIO.write(valueD); string valueStr = "spawnAt(100,200)"; binaryIO.writeString(valueStr); valueStr = "helpAt(32,3)"; binaryIO.writeString(valueStr); binaryIO.close(); }
Here’s how you would use it to READ:
string myPath = "some path to the same file"; if(binaryIO.open(myPath, BMBinaryIOMode::Read)) { cout << binaryIO.read<float>() << endl; cout << binaryIO.read<char>() << endl; double valueD = 0; binaryIO.read(valueD); // or you could use read<double() cout << valueD << endl; cout << binaryIO.readString() << endl; cout << binaryIO.readString() << endl; binaryIO.close(); }
You could even write/read a whole structure in 1 line:
struct Vertex { float x, y; }; Vertex vtx; vtx.x = 2.5f; vtx.y = 10.0f; // to write it binaryIO.write(vtx); // to read it Vertex vtxRead; binaryIO.read(vtxRead); // option 1 vtxRead = binaryIO.read<Vertex>(); // option 2