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?
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).