How to mimic C#’s BinaryReader / BinaryWriter in C++

Binary Reader Writer 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 struct 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
  Hope that helps, please don’t hesitate to provide feedback or drop a comment! 0

Leave a Reply

Your email address will not be published. Required fields are marked *