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

Popular posts from this blog

Boundaries

Overview

Considerations on an Optimization