Search code examples
c++ifstreamofstreambinaryreaderbinarywriter

I'd like to use ifstream and ofstream in C++ to mimic C#'s BinaryReader / BinaryWriter functionality


I'm looking for a way to write floats/ints/strings to a file and read them as floats/ints/strings. (basically read/write as ios::binary).


Solution

  • I ended up writing it myself. Just wanted to share it with others.

    It might not be optimized, but I had some difficulties finding C++ code that mimics C#'s BinaryReader & BinaryWriter classes. So I created one class that handles both read and write.

    Quick things to note:

    1) "BM" is just a prefix for my classes.

    2) BMLogging is a helper class that simply does:

    cout << "bla bla bla" << endl;
    

    So you can ignore the calls to BMLogging, I kept them to highlight the cases where we could warn the user.

    Here's the code:

    #include <iostream>
    #include <fstream>
    
    using namespace std;
    
    // Create the macro so we don't repeat the code over and over again.
    #define BMBINARY_READ(reader,value) reader.read((char *)&value, sizeof(value))
    
    enum BMBinaryIOMode
    {
        None = 0,
        Read,
        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())
            {
                BMLogging::error(BMLoggingClass::BinaryIO, "You forgot to call close() after finishing with the file! Closing it...");
                writer.close();
            }
    
            if(reader.is_open())
            {
                BMLogging::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;
    
            BMLogging::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, ios::binary);
                if(!writer.is_open())
                {
                    BMLogging::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, ios::binary);
                if(!reader.is_open())
                {
                    BMLogging::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();
            }
        }
    
        bool checkWritabilityStatus()
        {
            if(currentMode != BMBinaryIOMode::Write)
            {
                BMLogging::error(BMLoggingClass::BinaryIO, "Trying to write with a non Writable mode!");
                return false;
            }
            return true;
        }
    
        // Generic write method that will write any value to a file (except a string,
        // for strings use writeString instead).
        void write(void *value, size_t size)
        {
            if(!checkWritabilityStatus())
                return;
    
            // write the value to the file.
            writer.write((const char *)value, size);
        }
    
        // 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);
        }
    
        // helper to check if we're allowed to read
        bool checkReadabilityStatus()
        {
            if(currentMode != BMBinaryIOMode::Read)
            {
                BMLogging::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())
            {
                BMLogging::error(BMLoggingClass::BinaryIO, "Trying to read but reached the end of file!");
                reader.close();
                currentMode = BMBinaryIOMode::None;
                return false;
            }
    
            return true;
        }
    
        // reads a boolean value
        bool readBoolean()
        {
            if(checkReadabilityStatus())
            {
                bool value = false;
                BMBINARY_READ(reader, value);
                return value;
            }
    
            return false;
        }
    
        // reads a character value
        char readChar()
        {
            if(checkReadabilityStatus())
            {
                char value = 0;
                BMBINARY_READ(reader, value);
                return value;
            }
            return 0;
        }
    
        // read an integer value
        int readInt()
        {
            if(checkReadabilityStatus())
            {
                int value = 0;
                BMBINARY_READ(reader, value);
                return value;
            }
            return 0;
        }
    
        // read a float value
        float readFloat()
        {
            if(checkReadabilityStatus())
            {
                float value = 0;
                BMBINARY_READ(reader, value);
                return value;
            }
            return 0;
        }   
    
        // read a double value
        double readDouble()
        {
            if(checkReadabilityStatus())
            {
                double value = 0;
                BMBINARY_READ(reader, value);
                return value;
            }
            return 0;
        }
    
        // read a string value
        string readString()
        {
            if(checkReadabilityStatus())
            {
                char c;
                string result = "";
                while((c = readChar()) != '\0')
                {
                    result += c;
                }
                return result;
            }
            return "";
        }
    };
    

    EDIT: I replaced all the read/write methods above with these: (updated the usage code as well)

    // 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((c = read<char>()) != '\0')
            {
                result += c;
            }
            return result;
        }
        return "";
    }
    
    // read a string value
    void readString(string &result)
    {
        if(checkReadabilityStatus())
        {
            char c;
            result = "";
            while((c = read<char>()) != '\0')
            {
                result += c;
            }
        }
    }
    

    This is 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();
    }
    

    EDIT 2: 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
    

    Hope my code is clear enough.