Dear Kind DaniWebbers,

I am trying to overload the fstream operators (ofstream/ifstream), so that I can save a class to a 'Binary' file and also display it with cout. But the ways in which each need to be implemented is different. Please can you show me how to define different operators for each.

Here is my test class:

class CBase
{
public:
    //CBase(void) {};
    CBase(int i = 0, 
          float f = float(0.0), 
          double d = double(0.0), 
          char c = 'A') : 
          iMyInt(i), 
          fMyFloat(f), 
          dMyDouble(d), 
          cMyChar(c) {}
    ~CBase(void) {}

    int    iMyInt;
    float  fMyFloat;
    double dMyDouble;
    char   cMyChar;


    friend std::ostream& operator<<(std::ostream& os, CBase& obj)
    {
        os << obj.iMyInt << " " <<
              obj.fMyFloat <<  " " <<
              obj.dMyDouble <<  " " <<
              obj.cMyChar;

        //os.write((char *)&obj.iMyInt, sizeof(obj.iMyInt));
        //os.write((char *)&obj.fMyFloat, sizeof(obj.fMyFloat));
        //os.write((char *)&obj.dMyDouble, sizeof(obj.dMyDouble));
        //os.write((char *)&obj.cMyChar, sizeof(obj.cMyChar));

        return os;
    }

    friend std::istream& operator>>(std::istream& is, CBase& obj)
    {
        //is.read((char *)&obj.iMyInt, sizeof(obj.iMyInt));
        //is.read((char *)&obj.fMyFloat, sizeof(obj.fMyFloat));
        //is.read((char *)&obj.dMyDouble, sizeof(obj.dMyDouble));
        //is.read((char *)&obj.cMyChar, sizeof(obj.cMyChar));

        is >> obj.iMyInt >> 
              obj.fMyFloat >> 
              obj.dMyDouble >> 
              obj.cMyChar;

        return is;
    }
};

Thanks in advance.

Recommended Answers

All 4 Replies

Technically, you could just overload the operators for the classes std::ofstream and std::ifstream. Whenever the stream object is of a class derived from the file-stream classes, then it should pick that overload instead of the non-file-stream overloads, just because the file-stream classes are more derived (specialized) than the non-file-stream classes. So, you could just do this:

friend std::ostream& operator<<(std::ostream& os, const CBase& obj)
{
    os << obj.iMyInt << " " <<
          obj.fMyFloat <<  " " <<
          obj.dMyDouble <<  " " <<
          obj.cMyChar;
    return os;
}

friend std::ofstream& operator<<(std::ofstream& os, const CBase& obj)
{
    os.write((const char *)&obj.iMyInt, sizeof(obj.iMyInt));
    os.write((const char *)&obj.fMyFloat, sizeof(obj.fMyFloat));
    os.write((const char *)&obj.dMyDouble, sizeof(obj.dMyDouble));
    os.write((const char *)&obj.cMyChar, sizeof(obj.cMyChar));
    return os;
}

friend std::istream& operator>>(std::istream& is, CBase& obj)
{
    is >> obj.iMyInt >> 
          obj.fMyFloat >> 
          obj.dMyDouble >> 
          obj.cMyChar;
    return is;
}

friend std::ifstream& operator>>(std::ifstream& is, CBase& obj)
{
    is.read((char *)&obj.iMyInt, sizeof(obj.iMyInt));
    is.read((char *)&obj.fMyFloat, sizeof(obj.fMyFloat));
    is.read((char *)&obj.dMyDouble, sizeof(obj.dMyDouble));
    is.read((char *)&obj.cMyChar, sizeof(obj.cMyChar));
    return is;
}

The problem with this scheme, however, is that it won't work if, at some point, the file-stream objects get casted to non-file-stream class references. This could be problematic if you compose more complicated use-scenarios.

To solve that problem, you could use a dynamic_cast within the operators:

friend std::ostream& operator<<(std::ostream& os, const CBase& obj)
{
    std::ofstream* p_ofs = dynamic_cast<std::ofstream*>(&os);
    if( p_ofs == NULL ) {
        os << obj.iMyInt << " " <<
              obj.fMyFloat <<  " " <<
              obj.dMyDouble <<  " " <<
              obj.cMyChar;
    } else {
        p_ofs->write((const char *)&obj.iMyInt, sizeof(obj.iMyInt));
        p_ofs->write((const char *)&obj.fMyFloat, sizeof(obj.fMyFloat));
        p_ofs->write((const char *)&obj.dMyDouble, sizeof(obj.dMyDouble));
        p_ofs->write((const char *)&obj.cMyChar, sizeof(obj.cMyChar));
    };
    return os;
}

friend std::istream& operator>>(std::istream& is, CBase& obj)
{
    std::ifstream* p_ifs = dynamic_cast<std::ifstream*>(&is);
    if( p_ifs == NULL ) {
        is >> obj.iMyInt >> 
              obj.fMyFloat >> 
              obj.dMyDouble >> 
              obj.cMyChar;
    } else {
        p_ifs->read((char *)&obj.iMyInt, sizeof(obj.iMyInt));
        p_ifs->read((char *)&obj.fMyFloat, sizeof(obj.fMyFloat));
        p_ifs->read((char *)&obj.dMyDouble, sizeof(obj.dMyDouble));
        p_ifs->read((char *)&obj.cMyChar, sizeof(obj.cMyChar));
    };
    return is;
}

But, of course, that will cost you the price of a dynamic-cast, which may not be much.

To be honest, if you really want to implement a good file reading/writing capabilities for your classes, I recommend you use or create a serialization library, with its own stream-like classes that have the required under-the-hood dynamic mechanisms to handle the different formats you might want to output to (binary, XML, proto-buf, JSON, etc..). A good example of this is the Boost.Serialization library. The problem is really that the standard stream classes are not really equipped to do this kind of work, and you will have to end up creating code like the two options above, which is not really practical or scalable.

Lines 23 and 43 write text files, not binary files. If you want binary file then. You can't write binary then expect to read it as text, or vice versa.

os.write((void*)&os, sizeof(os));

and

os.read((void*)&os, sizeof(os));

On MS_Windows it's dangerous to attempt to write both text and binary into the same file because of the way '\n' is physically written to the hard drive -- it's actually written as two bytes, not one. *nix doesn't have that problem.

Thanks @Mike_2000_17
Thanks @Ancient\nDragon

Great answers I really appreciate it. I was just reading the tutorials for boost.Serialization, funny that.

Whether the output is going to (input is coming from) a file or not is determined by the dynamic type of the streambuf object, and not by the dynamic type of the stream object. (It is not unusual in C++ to do runtime redirection via the stream buffer.)

Discriminating on the dynamic type of the streambuf results in more robust code.

#include <iostream>
#include <fstream>

struct A { /* whatever */ };
struct B { /* whatever */ };

namespace detail_
{
    std::ostream& puta( std::ostream& stm, A, std::false_type )
    { return stm << "write to non-file: A{ /* ... */ }" ; }

    std::ostream& puta( std::ostream& stm, A, std::true_type )
    { return stm << "write to file: A{ /* ... */ }" ; }

    std::ostream& putb( std::ostream& stm, B, std::false_type )
    { return stm << "write to non-file: B{ /* ... */ }" ; }

    std::ostream& putb( std::ostream& stm, B, std::true_type )
    { return stm << "write to file: B{ /* ... */ }" ; }
}

std::ostream& operator<< ( std::ostream& stm, A a )
{
    auto p = dynamic_cast<std::filebuf*>(stm.rdbuf()) ;
    return p ? detail_::puta( stm , a, std::true_type{} ) :
                detail_::puta( stm , a, std::false_type{} ) ;
}

std::ostream& operator<< ( std::ostream& stm, B b )
{
    auto p = dynamic_cast<std::ofstream*>( std::addressof(stm) ) ;
    return p ? detail_::putb( stm , b, std::true_type{} ) :
                detail_::putb( stm , b, std::false_type{} ) ;
}

int main()
{
    A a ;
    B b ;

    {
        std::filebuf fbuf ;
        fbuf.open( "some_file.txt", std::ios::out ) ;
        std::ostream ostm(&fbuf) ;
        ostm << a << '\n' ; // the right overload is called
        ostm << b << '\n' ; // the wrong overload is called
    }
}

Snippet: http://coliru.stacked-crooked.com/a/c7d5c02fbe30808f

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.