c++ custom output stream with indentation
Asked Answered
H

2

9

I'm having some trouble trying to implement a custom stream class to generate nicely indented code in an output file. I've searched online extensively but there doesn't seem to be a consensus on the best way to achieve this. Some people talk about deriving the stream, others talk about deriving the buffer, yet others suggest the use of locales/facets etc.

Essentially, I'm finding myself writing a lot of code like this:

ofstream myFile();
myFile.open("test.php");
myFile << "<html>" << endl <<
          "\t<head>" << endl <<
          "\t\t<title>Hello world</title>" << endl <<
          "\t</head>" << endl <<
          "</html>" << endl;

When the tabs start to add up it looks horrible, and it seems like it would be nice to have something like this:

ind_ofstream myFile();
myFile.open("test.php");
myFile << "<html>" << ind_inc << ind_endl <<
          "<head>" << ind_inc << ind_endl <<
          "<title>Hello world</title>" << ind_dec << ind_endl <<
          "</head>" << ind_dec << ind_endl <<
          "</html>" << ind_endl;

i.e. create a derived stream class which would keep track of its current indent depth, then some manipulators to increase/decrease the indent depth, and a manipulator to write a newline followed by however many tabs.

So here's my shot at implementing the class & manipulators:

ind_ofstream.h

class ind_ofstream : public ofstream
{
    public:
        ind_ofstream();
        void incInd();
        void decInd();
        size_t getInd();

    private:
        size_t _ind;
};

ind_ofstream& inc_ind(ind_ofstream& is);
ind_ofstream& dec_ind(ind_ofstream& is);
ind_ofstream& endl_ind(ind_ofstream& is);

ind_ofstream.cpp

ind_ofstream::ind_ofstream() : ofstream()   {_ind = 0;}
void ind_ofstream::incInd()     {_ind++;}
void ind_ofstream::decInd()     {if(_ind > 0 ) _ind--;}
size_t ind_ofstream::getInd()       {return _ind;}

ind_ofstream& inc_ind(ind_ofstream& is)     
{ 
    is.incInd();
    return is; 
}

ind_ofstream& dec_ind(ind_ofstream& is)     
{ 
    is.decInd();
    return is; 
}

ind_ofstream& endl_ind(ind_ofstream& is)    
{
    size_t i = is.getInd();
    is << endl;
    while(i-- > 0) is << "\t";
    return is;
}

This builds, but doesn't generate the expected output; any attempt to use the custom manipulators results in them being cast to a boolean for some reason and "1" written to the file. Do I need to overload the << operator for my new class? (I haven't been able to find a way of doing this that builds)

Thanks!

p.s.

1) I've omitted the #includes, using namespace etc from my code snippets to save space.

2) I'm aiming to be able to use an interface similar to the one in my second code snippet. If after reading the whole post, you think that's a bad idea, please explain why and provide an alternative.

Henkel answered 12/12, 2012 at 15:10 Comment(2)
Question: if the desired outcome is clean code and correct output - that is, if this is not merely academic or for your own betterment - why write [HT|X]ML directly this way? At the least, you could write it to disc UNindented, then use some prettifier (tidy, for instance) to do this dirty work. ...that being said, it is interesting, and I have a greasy feeling I'll be cobbling a solution together soon. :)Reisfield
Hi - that's a good idea and probably what I'll end up doing if I can't get Plan A to work. I find streams to be one of the more confusing aspects of C++, so I thought this might be a good way to get a deeper understanding of how they work...Henkel
U
8

The iostreams support adding custom data to them, so you don't need to write a full derived class just to add an indentation level that will be operated on by manipulators. This is a little-known feature of iostreams, but comes in handy here.

You would write your manipulators like this:

/* Helper function to get a storage index in a stream */
int get_indent_index() {
    /* ios_base::xalloc allocates indices for custom-storage locations. These indices are valid for all streams */
    static int index = ios_base::xalloc();
    return index;
}

ios_base& inc_ind(ios_base& stream) {
    /* The iword(index) function gives a reference to the index-th custom storage location as a integer */
    stream.iword(get_indent_index())++;
    return stream;
}

ios_base& dec_ind(ios_base& stream) {
    /* The iword(index) function gives a reference to the index-th custom storage location as a integer */
    stream.iword(get_indent_index())--;
    return stream;
}

template<class charT, class traits>
basic_ostream<charT, traits>& endl_ind(basic_ostream<charT, traits>& stream) {
    int indent = stream.iword(get_indent_index());
    stream.put(stream.widen('\n');
    while (indent) {
        stream.put(stream.widen('\t');
        indent--;
    }
    stream.flush();
    return stream;
}
Unbending answered 12/12, 2012 at 15:59 Comment(0)
Z
1

I have combined Bart van Ingen Schenau's solution with a facet, to allow pushing and popping of indentation levels to existing output streams. The code is available on github: https://github.com/spacemoose/ostream_indenter, and there's a more thorough demo/test in the repository, but basically it allows you to do the following:

/// This probably has to be called once for every program:
// https://mcmap.net/q/282732/-how-can-i-use-std-imbue-to-set-the-locale-for-std-wcout
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" << indent_manip::pop;

To produce:

I want to push indentation levels:
    To arbitrary depths
        and pop them
    back down
like this.

I had to do a kind of nasty trick, so I'm interested in hearing feedback on the codes utility.

Zarzuela answered 11/9, 2015 at 13:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.