Search code examples
c++streamifstreamstreambuf

Why don't inherit from ifstream


I want to create a custom input file stream which automatically strips comments and other garbage data. I came up with the following solution:

class FileReader : public std::ifstream
{
public:
  explicit FileReader(const char* fName) { open(fName); }

  ~FileReader() { if (is_open()) close(); }

  template <typename T, bool IsBaseOfSerializable>
  struct DoRead
  {
    void operator()(std::ifstream& ifs, T& data) { ifs >> data; }
  };

  template <typename T>
  struct DoRead<T, true>
  {
    void operator()(FileReader& reader, T& data) { data.Deserialize(reader); }
  };

  template <typename T>
  friend FileReader& operator>>(FileReader& reader, T& data)
  {
    reader.SkipCommentsAndGarbage();
    DoRead<T, std::is_base_of<ISerializable, T>::value> doread;
    doread(reader, data);
    return reader;
  }

  void SkipCommentsAndGarbage() {... }
};

I also have interface ISerializable containing Serialize/Deserialize methods. Everything looks fine with me.

But I have read that I should never inherit from std::ifstream and should create custom std::streambuf.

Could you please explain why is it bad to inherit from std::ifstream and how can I create custom std::streambuf which ignores comments and other data in similar way?


Solution

  • It's not clear to me how you expect your class to work. The operator>> functions are not virtual in std::istream, and sooner or later (generally sooner, in well written code), you'll end up with a std::istream&. You're idea for forwarding can be used in some cases, but in such cases, you don't inherit from std::ifstream; you contain a pointer to an istream, and forward to it. (By not inheriting, you ensure that you can't end up with a istream&. Which is limiting, but acceptable in certain cases.)

    The normal way of doing this is to provide a filtering streambuf, which filters the input text. For example, if comments are from a # to the end of the line, and you don't have to worry about quotes and such, something as simple as the following will work:

    class UncommentStreambuf : public std::streambuf
    {
        std::streambuf* mySource;
        std::istream*   myOwner;
        char            myBuffer;
    protected:
        int underflow() override
        {
            int results = mySource->sbumpc();
            if ( results == '#' ) {
                while ( mySource->sgetc() != '\n' ) {
                    mySource->sbumpc();
                }
            }
            if (results != traits_type::eof()) {
                myBuffer = results;
                setg( &myBuffer, &myBuffer, &myBuffer + 1 );
            } else {
                setg( nullptr, nullptr, nullptr );
            }
            return results;
        }
    public:
        UncommentStreambuf( std::streambuf* source )
            : mySource( source )
            , myOwner( nullptr )
        {
        }
        UncommentStreambuf( std::istream& source )
            : mySource( source.rdbuf() )
            , myOwner( &source )
        {
            source.rdbuf( this );
        }
        ~UncommentStreambuf()
        {
            if ( myOwner != nullptr ) {
                myOwner->rdbuf( mySource );
            }
        }
    };
    

    If you need to handle other types of comments, or worry about quoted comment characters, you'll need more logic (with possibly a private buffer to collect characters, in order to test for a sequence of more than one character).