Reputation: 249
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?
Upvotes: 0
Views: 897
Reputation: 27577
It's basically the std::iostream
design to customise std::streambuf
, that's where all context specific functionality resides.
Upvotes: 1
Reputation: 153929
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).
Upvotes: 4
Reputation: 11116
Because publicly inheriting to modify the behavior of ifstream
will lead to unintended consequences, because it still is an istream
and will be treated as such in many cases - stripping away your custom behavior:
When passing FileReader
to a function that expects an ::std::istream&
, your custom functionality will not be used
Any class C
with an operator like template<typename T> T& operator>>(T& in, C& target);
will be unusable
Whenever any functionality of the underlying istream
is used directly, your wrapper functions do not work
In your case, you could simply switch to having your ifstream
as a member or change its inheritance to private.
Upvotes: 3