11
\$\begingroup\$

I've always wanted to be able to steer stream indentation, so that I could write code like this:

/// This probably has to be called once for every program:
// http://stackoverflow.com/questions/26387054/how-can-i-use-stdimbue-to-set-the-locale-for-stdwcout
std::ios_base::sync_with_stdio(false);
std::cout << "I want to push indentation levels:\n" << indent_manip::push
          << "To arbitrary depths\n" << indent_manip::push
          << "and pop them\n" << indent_manip::pop
          << "back down\n" << indent_manip::pop
          << "like this.\n"

Finally, I've done it, but I had to do a kind of nasty hack.

The problem is:

  1. I need to call a facet that is called on every string, which appears to mean that I have to use codecvt.
  2. I need the indentation level to persist, which means using iosbase::iword.
  3. codecvt doesn't have a reference to iosbase anywhere.

Combine all that with the fact that facets have to be constant objects, and you get a kind of tricky problem. I solved it with a dirty trick:

  • I use iword to keep track of the indentation level, since I get stream tracking for free that way. If I was only dealing with a single stream it would useless information.
  • Whenever indent_manip::push is called, the iword is incremented, a new fact is created, a locale is created with that facet, and the current stream is imbued with that locale.

I put the implementation, along with a more involved demo/test on GitHub.

Tested on gcc 5.1 with:

g++ -o indenter  --std=c++14 indent_test.cpp

What I'm most interested in

Obviously it's not going to win performance prizes, and there's some easy places to optimize. I'm curious about the safety of what I'm trying to do. Is there a cleaner alternative?

Obviously I can create a custom ostream, and that'll be phase two, but I have a use case where I want to imbue this behavior into an existing std::ostream, and this was the only way I could figure out how to do it.

How's the usability?

#pragma once

#include <locale>
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <cassert>



class indent_facet : public std::codecvt<char, char, std::mbstate_t> {
public:
    explicit indent_facet( int indent_level, size_t ref = 0)
        : std::codecvt<char, char, std::mbstate_t>(ref), m_indentation_level(indent_level) {}
    typedef std::codecvt_base::result result;
    typedef std::codecvt<char, char, std::mbstate_t> parent;
    typedef parent::intern_type intern_type;
    typedef parent::extern_type extern_type;
    typedef parent::state_type  state_type;

    int &state(state_type &s) const { return *reinterpret_cast<int *>(&s); }

protected:
    virtual result do_out(state_type &need_indentation,
        const intern_type *from, const intern_type *from_end, const intern_type *&from_next,
        extern_type *to, extern_type *to_end, extern_type *&to_next
        ) const override;

    // Override so the do_out() virtual function is called.
    virtual bool do_always_noconv() const throw() override {
        return m_indentation_level==0;
    }
    unsigned int m_indentation_level = 0;

};

indent_facet::result indent_facet::do_out(state_type &need_indentation,
    const intern_type *from, const intern_type *from_end, const intern_type *&from_next,
    extern_type *to, extern_type *to_end, extern_type *&to_next
    ) const
{
    result res = std::codecvt_base::noconv;
    for (; (from < from_end) && (to < to_end); ++from, ++to) {
        // 0 indicates that the last character seen was a newline.
        // thus we will print a tab before it. Ignore it the next
        // character is also a newline
        if ((state(need_indentation) == 0) && (*from != '\n')) {
            res = std::codecvt_base::ok;
            state(need_indentation) = 1;
            for(int i=0; i<m_indentation_level; ++i){
                *to = '\t'; ++to;
            }
            if (to == to_end) {
                res = std::codecvt_base::partial;
                break;
            }
        }
        *to = *from; // Copy the next character.

        // If the character copied was a '\n' mark that state
        if (*from == '\n') {
            state(need_indentation) = 0;
        }
    }

    if (from != from_end) {
        res = std::codecvt_base::partial;
    }
    from_next = from;
    to_next = to;

    return res;
};



/// I hate the way I solved this, but I can't think of a better way
/// around the problem.  I even asked stackoverflow for help:
///
///   http://stackoverflow.com/questions/32480237/apply-a-facet-to-all-stream-output-use-custom-string-manipulators
///
///
namespace  indent_manip{

static const int index = std::ios_base::xalloc();

static std::ostream & push(std::ostream& os)
{
    auto ilevel = ++os.iword(index);
    os.imbue(std::locale(os.getloc(), new indent_facet(ilevel)));
    return os;
}

std::ostream& pop(std::ostream& os)
{
    auto ilevel = (os.iword(index)>0) ? --os.iword(index) : 0;
    os.imbue(std::locale(os.getloc(), new indent_facet(ilevel)));
    return os;
}

/// Clears the ostream indentation set, but NOT the raii_guard.
std::ostream& clear(std::ostream& os)
{
    os.iword(index) = 0;
    os.imbue(std::locale(os.getloc(), new indent_facet(0)));
    return os;
}



/// Provides a RAII guard around your manipulation.
class raii_guard
{
public:
    raii_guard(std::ostream& os):
        start_level(os.iword(index)),
        oref(os)
    {}

    ~raii_guard()
    {
        reset();
    }

    /// Resets the streams indentation level to the point itw as at
    /// when the guard was created.
    void reset()
    {
        oref.iword(index) = start_level;
        oref.imbue(std::locale(oref.getloc(), new indent_facet(start_level)));
    }

private:
    std::ostream& oref;
     int start_level;
};

}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Don't think too much about a custom ostream. I think it's a lot more useful if you can imbue it on any std::ostream. \$\endgroup\$
    – user23573
    Sep 28, 2015 at 12:00

1 Answer 1

2
\$\begingroup\$

Warning: this answer is really more of an SO-style answer about the right way to solve this problem than it is a review of this code. I apologize for that, but I do hope you'll forgive me, and perhaps even read on to see why.

Neither a facet nor a custom ostream is the right way to solve this problem. Your question on SO (which I'm sorry I missed when it was posted) is (at least in my opinion) nearly a perfect example of the XY problem. You asked something very specific about facets, but the reality is that a facet just isn't the right tool for the job.

So, having said nasty things (sorry 'bout that) about the approach you've taken, what would I do instead? I'd recommend a custom streambuf. It's basically a complete rewrite from the ground up, so I'm not going to spend a lot of effort on reviewing your code as it stands right now. Instead, I'll try to point you in a direction I think is better, with a bit of code I wrote some time ago. It's not quite exactly what you're trying to do, but it's similar, and does show a better way to integrate the basic capability into iostreams.

#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; 
    }
};

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.";
}
\$\endgroup\$
2
  • \$\begingroup\$ I don't think it's an XY problem... as I say towards the end of my post: "Obviously I can create a custom ostream, and that'll be phase two, but I have a use case where I want to imbue this behavior into an existing std::ostream, and this was the only way I could figure out how to do it.". The idea is to be able to indent existing output in code I don't want to/can't modify. \$\endgroup\$
    – Spacemoose
    Oct 14, 2015 at 14:01
  • 1
    \$\begingroup\$ @Spacemoose: Yes, this supports attaching the streambuf to an existing stream (the widthstream class is convenient, but not really needed). Although I had this code handy, I should add that for what you want (just indenting) the code can actually be a lot simpler as well. \$\endgroup\$ Oct 14, 2015 at 15:15

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.