1

I would like to know if it is possible to inherit from std::ostream, and to override flush() in such a way that some information (say, the line number) is added to the beginning of each line. I would then like to attach it to a std::ofstream (or cout) through rdbuf() so that I get something like this:

ofstream fout("file.txt");
myostream os;
os.rdbuf(fout.rdbuf());

os << "this is the first line.\n";
os << "this is the second line.\n";

would put this into file.txt

1 this is the first line.
2 this is the second line.
John
  • 1,709
  • 1
  • 24
  • 27

2 Answers2

5

flush() wouldn't be the function to override in this context, though you're on the right track. You should redefine overflow() on the underlying std::streambuf interface. For example:

class linebuf : public std::streambuf
{
public:
    linebuf() : m_sbuf() { m_sbuf.open("file.txt", std::ios_base::out); }

    int_type overflow(int_type c) override
    {
        char_type ch = traits_type::to_char_type(c);
        if (c != traits_type::eof() && new_line)
        {
            std::ostream os(&m_sbuf);
            os << line_number++ << " ";
        }

        new_line = (ch == '\n');
        return m_sbuf.sputc(ch);
    }

    int sync() override { return m_sbuf.pubsync() ? 0 : -1; }
private:
    std::filebuf m_sbuf;
    bool new_line = true;
    int line_number = 1;
};

Now you can do:

linebuf buf;
std::ostream os(&buf);

os << "this is the first line.\n";  // "1 this is the first line."
os << "this is the second line.\n"; // "2 this is the second line."

Live example

David G
  • 94,763
  • 41
  • 167
  • 253
  • 1
    Is this solution usable for outputting to a sstream? – M.M May 15 '14 at 22:00
  • 1
    @MattMcNabb Not really; the buffer is a wrapper around a `std::filebuf`, so it can only be used on files. If you have a variety of stream types for which you want this to work, use a `std::streambuf*` and initialize it with the buffer of the stream through the constructor. – David G May 15 '14 at 22:11
  • 1
    This could be a James Kanze-style streambuf filter if you replace `std::filebuf m_sbuf;` with `std::streambuf* m_sbuf;`. – aschepler May 15 '14 at 22:55
  • One follow-up question: it seems that nothing is written after I use `std::endl`. Do I need to treat these separate much like doing `operator<<(ostream& (*func)(ostream&))`? – John May 16 '14 at 14:37
  • @Johann In addition to writing `\n` to the stream, `std::endl` also flushes the stream. So I would avoid using that manipulator unless you have to. – David G May 16 '14 at 15:19
  • @Johann [Here's an example that works with `std::endl`](http://coliru.stacked-crooked.com/a/2d040a2b46a14998) – David G May 17 '14 at 16:21
2

James Kanze's classic article on Filtering Streambufs has a very similar example which puts a timestamp at the beginning of every line. You could adapt that code.

Or, you could use the Boost tools that grew out of the ideas in that article.

#include <boost/iostreams/filtering_stream.hpp>
#include <boost/array.hpp>
#include <cstring>
#include <limits>

// line_num_filter is a model of the Boost concept OutputFilter which
// inserts a sequential line number at the beginning of every line.
class line_num_filter
    : public boost::iostreams::output_filter
{
public:
    line_num_filter();

    template<typename Sink>
    bool put(Sink& snk, char c);

    template<typename Device>
    void close(Device&);

private:
    bool m_start_of_line;
    unsigned int m_line_num;
    boost::array<char, std::numeric_limits<unsigned int>::digits10 + 4> m_buf;
    const char* m_buf_pos;
    const char* m_buf_end;
};

line_num_filter::line_num_filter() :
    m_start_of_line(true),
    m_line_num(1),
    m_buf_pos(m_buf.data()),
    m_buf_end(m_buf_pos)
{}

// put() must return true if c was written to dest, or false if not.
// After returning false, put() with the same c might be tried again later.
template<typename Sink>
bool line_num_filter::put(Sink& dest, char c)
{
    // If at the start of a line, print the line number into a buffer.
    if (m_start_of_line) {
        m_buf_pos = m_buf.data();
        m_buf_end = m_buf_pos +
            std::snprintf(m_buf.data(), m_buf.size(), "%u ", m_line_num);
        m_start_of_line = false;
    }

    // If there are buffer characters to be written, write them.
    // This can be interrupted and resumed if the sink is not accepting
    // input, which is why the buffer and pointers need to be members.
    while (m_buf_pos != m_buf_end) {
        if (!boost::iostreams::put(dest, *m_buf_pos))
            return false;
        ++m_buf_pos;
    }

    // Copy the actual character of data.
    if (!boost::iostreams::put(dest, c))
        return false;

    // If the character copied was a newline, get ready for the next line.
    if (c == '\n') {
        ++m_line_num;
        m_start_of_line = true;
    }
    return true;
}

// Reset the filter object.
template<typename Device>
void line_num_filter::close(Device&)
{
    m_start_of_line = true;
    m_line_num = 1;
    m_buf_pos = m_buf_end = m_buf.data();
}


int main() {
    using namespace boost::iostreams;
    filtering_ostream myout;
    myout.push(line_num_filter());
    myout.push(std::cout);

    myout << "this is the first line.\n";
    myout << "this is the second line.\n";
}
aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Thank you for this. Boost would simplify cases where I want the option to change filtering at run-time. I'll have to see if a filtering_ostream could be stored as a shared_ptr member variable in a class. – John May 16 '14 at 14:21
  • 1
    Yes, `filtering_ostream` inherits `std::ostream` (and both have public virtual destructors). – aschepler May 16 '14 at 14:38