6
\$\begingroup\$

Today's code is a filtering streambuf (and associated stream type) to automate word-wrapped output in C++:

#include <iostream>
#include <streambuf>
#include <iomanip>

class widthbuf: public std::streambuf {
public:
    widthbuf(int w, std::streambuf* s)
        : indent_width(0), 
        def_width(w), 
        width(w), 
        sbuf(s), 
        count(0) 
    {}

    ~widthbuf() { overflow('\n'); }

    void set_indent(int w) { 
        if (w == 0) {
            prefix.clear();
            indent_width = 0;
            width = def_width;
        }
        else {
            indent_width += w; 
            prefix = string(indent_width, space);
            width -= w; 
        }
    }

private:

    typedef std::basic_string<char_type> string;

    // This is basically a line-buffering stream buffer.
    // The algorithm is: 
    // - Explicit end of line ("\r" or "\n"): we flush our buffer 
    //   to the underlying stream's buffer, and set our record of
    //   the line length to 0.
    // - An "alert" character: sent to the underlying stream
    //   without recording its length, since it doesn't normally
    //   affect the a appearance of the output.
    // - tab: treated as occupying `tab_width` characters, but is 
    //   passed through undisturbed (but if we wanted to expand it
    //   to `tab_width` spaces, that would be pretty easy to do so
    //   you could adjust the tab-width if you wanted.
    // - Everything else: really basic buffering with word wrapping. 
    //   We try to add the character to the buffer, and if it exceeds
    //   our line width, we search for the last space/tab in the 
    //   buffer and break the line there. If there is no space/tab, 
    //   we break the line at the limit.
    int_type overflow(int_type c) {
        if (traits_type::eq_int_type(traits_type::eof(), c))
            return traits_type::not_eof(c);
        switch (c) {
        case '\n':
        case '\r': 
            {
                buffer += c;
                count = 0;
                sbuf->sputn(prefix.c_str(), indent_width);
                int_type rc = sbuf->sputn(buffer.c_str(), buffer.size());
                buffer.clear();
                return rc;
            }
        case '\a':
            return sbuf->sputc(c);
        case '\t':
            buffer += c;
            count += tab_width - count % tab_width;
            return c;
        default:
            if (count >= width) {
                size_t wpos = buffer.find_last_of(" \t");
                if (wpos != string::npos) {
                    sbuf->sputn(prefix.c_str(), indent_width);
                    sbuf->sputn(buffer.c_str(), wpos);
                    count = buffer.size()-wpos-1;
                    buffer = string(buffer, wpos+1);
                }
                else {
                    sbuf->sputn(prefix.c_str(), indent_width);
                    sbuf->sputn(buffer.c_str(), buffer.size());
                    buffer.clear();
                    count = 0;
                }
                sbuf->sputc('\n');
            }
            buffer += c;
            ++count;
            return c;
        }
    }

    size_t indent_width;
    size_t width, def_width;
    size_t count;
    size_t tab_count;
    static const int tab_width = 8;
    string prefix;

    char_type space = static_cast<char_type>(' ');

    std::streambuf* sbuf;

    string buffer;
};

class widthstream : public std::ostream {
    widthbuf buf;
public:
    widthstream(size_t width, std::ostream &os) 
        : buf(width, os.rdbuf()), 
        std::ostream(&buf) 
    {}

    widthstream &indent(int w) { 
        buf.set_indent(w); 
        return *this; 
    }
};

#ifdef TEST

int main() {
    widthstream out(30, std::cout);
    out.indent(10) << "This is a very long string that will wrap to the"
        " next line because it is a very long string that will wrap to "
        "the next line.\n";
    out.indent(0) << "This is\tsome\tmore text that should not be indented "
        "but should still be word wrapped to 30 columns.";
}

#endif

As you can see from the included demo/test code, along with word-wrapping to a specified width, this supports arbitrary indentation.

\$\endgroup\$
1
  • \$\begingroup\$ I would create std::codecvt facet specialization. Then you can apply it to any stream you like. Here is one for indenting \$\endgroup\$ Jun 21, 2014 at 3:08

1 Answer 1

5
\$\begingroup\$

I looked over this code carefully and came up with a number of aspects that might be considered for future enhancements.

Write initializers in declaration order

Since initialization is done in declaration order rather than the order that initializers are written, it's helpful to readers of your code to make sure that initialization order matches declaration order. In the widthbuf constructor, the code is like this:

widthbuf(int w, std::streambuf* s)
    : indent_width(0), 
    def_width(w), 
    width(w), 
    sbuf(s), 
    count(0) 
{}

But because of their declaration order, width actually gets initialized before def_width and count before sbuf, so it would be one fewer opportunity for misunderstanding if these were arranged in declaration order.

Don't rely on promises not actually made

The constructor order comment also applies to the constructor of widthstream and the order of the underlying base class std::ostream and widthstream::buf but in this particular case it's not actually guaranteed to work:

widthstream(size_t width, std::ostream &os) 
    : buf(width, os.rdbuf()),
    std::ostream(&buf)
{}

It looks as though the intent was to initialize buf first and then use it to construct the underlying std::ostream but that's not actually what happens. In fact, the code is passing a reference to a not-yet-fully-constructed buf which is unusual but not technically illegal as long as the constructor receiving the reference does not do anything with it, such as dereference it, attempt to access member functions, etc. Because the underlying base class std::ostream makes no such guarantee not to dereference, this code is prone to failure.

This can be easily demonstrated by replacing std::ostream in widthstream with evil_ostream which attempts to get the locale of the std::streambuf that is the base class of widthbuf:

class evil_ostream : public std::ostream {
public:
    evil_ostream(std::streambuf *buf) : std::ostream(buf) { 
        std::cout << "(ab)using reference to unconstructed buf\n"; 
        auto loc = buf->getloc();   // kaboom!
    }
    virtual ~evil_ostream() {};
};

The line that attempts to fetch the locale causes a segfault because the underlying object is not actually completely constructed.

Handle wstring or state that you don't

The code does not seem to be happy with std::wstring instead of std::string as input. This isn't necessarily a problem, but it should be stated, at least in comments that wide characters are not handled by this code. Alternatively, adding support for wchar_t strings could probably be done with the use of templates.

Use all of the required #includes

The type std::basic_string<char_type> is used but its declaration is in #include <string> which is not actually in the list of includes.

Consider using facets

As mentioned by @LokiAstari in comments, it might be appropriate to consider the use of a facet for this purpose. The reasons one might do so would include the fact that formatting of text representation is a particular strength of the facet approach. However, there are also reasons one might not do so, including the fact that facets typically incorporate a cultural apect that isn't really appropos here and that facets tend to have complementary input and output aspects which also aren't really gemane to this application.

\$\endgroup\$
1
  • \$\begingroup\$ For your second section, the solution would be to use the Base from Member idiom - the wiki example of which even uses ostream! \$\endgroup\$
    – Barry
    May 6, 2016 at 16:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.