Revisiting the FileLineSource utility
A number of posts ago, I talked about a small utility called FileLineSource, which did a little bit of complexity hiding when reading lines from a text file.
That utility had one implicit issue: it perpetuated the interface of std::getline(), for which it was, essentially, a drop-in replacement. That meant that it was entirely functional, but it did not support the use of STL algorithms.
Thus its use was something like this:
auto source = inFileFactory.create();
source->openSource();
std::string s;
while (source->getNextLine(s))
m_records.emplace_back(s);
This could be improved upon. It uses a C-style while loop, it's spread out over five lines, and it needs a bit of attention to see what it's doing.
The way to address this was not to change the utility itself, but to add a wrapper. Because the use for this corresponds to an input range, the simplest of ranges, we can provide a very simple wrapper which provides the missing range functionality.
class FileLineSourceRange
{
public:
class Iterator
{
public:
using difference_type = std::ptrdiff_t;
using value_type = std::string;
Iterator(IFileLineSource &inSource):
m_source(&inSource), m_status(m_source->getNextLine(m_value))
{ }
bool operator==(const bool inVal) const { return m_status == inVal; }
const std::string &operator*() const { return m_value; }
Iterator &operator++(); // pre-incrementable
void operator++(int) // post-incrementable
{
++*this;
}
private:
IFileLineSource *m_source;
std::string m_value;
bool m_status;
};
FileLineSourceRange(const IFileLineSourceFactory &inFactory);
Iterator begin() { return Iterator(*m_source); }
bool end() { return false; }
private:
std::unique_ptr<IFileLineSource> m_source;
};
This is very minimal -- it uses the C++20 ability to use a simple sentinel to mark the end of a range. (Pre-C++20 I would have had to define an empty iterator with a status of false. and an extra constructor, to make it work. Here it's just a boolean flag returned by end().)
The only two functions defined in the cpp file (for insulation purposes) are very simple:
auto FileLineSourceRange::Iterator::operator++() -> Iterator &
{
if (m_status)
{
m_status = m_source->getNextLine(m_value);
}
return *this;
}
FileLineSourceRange::FileLineSourceRange(
const IFileLineSourceFactory &inFactory):
m_source(inFactory.create())
{ }
After adding a unit test (which caught a bug in the initial condition) this was integrated and changes made to the call sites in the code base. That five-line call site referenced above now looks like:
std::ranges::transform(
JSBUtil::FileLineSourceRange(inFileFactory),
std::back_inserter(m_records),
[](const std::string &inStr) { return BaseLibraryBookRecord{ inStr }; });
}
One statement, clear about what it's doing.
Comments
Post a Comment