Grzenio
Grzenio

Reputation: 36679

Printing of structured objects with correct indentations using streams in C++

I would like to write the << operators for my classes, which in general contain some structured data. One of the easiest examples is a class containing a list of matrices, e.g.

stuct MyClass
{
  std::vector<Matrix> m;
}

assume that the Matrix class can print itself (using multiple lines), and that we don't control this class. Now I would like to get the following output:

A(0)=[a b
      c d]
A(1)=[e f
      g h]

and that the Matrix class itself outputs:

[a b
 c d]

how can configure/change the stream or do something completely different to make it work, i.e. to make the stream output a given number of spaces after every std::endl?

Upvotes: 2

Views: 210

Answers (1)

James Kanze
James Kanze

Reputation: 154007

This is actually the first real use I made of filtering streambuf's. Given something like:

std::ostream& operator<<( std::ostream& dest,
                          std::vector<Matrix> const& object );

class MyClass
{
    std::vector<Matrix> m;
};

std::ostream& operator<<( std::ostream& dest, MyClass const& object )
{
    //  ...
    {
        //  Scope to control lifetime of the IndentingStreambuf...
        //  Could be (probably should be) in a separate function.
        IndentingStreambuf indent( dest )
        dest << m;
    }
    //  ...
}

Where:

class IndentingStreambuf : public std::streambuf
{
    std::streambuf* myDest;
    std::ostream*   myOwner;
    bool            myIsAtStartOfLine;

protected:
    int overflow( int ch )
    {
        if ( myIsAtStartOfLine && ch != EOF && ch != '\n' ) {
            myDest->sputn( "    ", 4 );
        }
        myIsAtStartOfLine = ch == '\n';
        return myDest->sputc( ch );
    }

public:
    IndentingStreambuf( std::streambuf* dest )
        : myDest( dest )
        , myOwner( NULL )
        , myIsAtStartOfLine( true )
    {
    }
    IndentingStreambuf( std::ostream& dest )
        : myDest( dest.rdbuf() )
        , myOwner( &dest )
        , myIsAtStartOfLine( true )
    {
        myOwner->rdbuf( this );
    }
    ~IndentingStreambuf()
    {
        if ( myOwner != NULL ) {
            myOwner->rdbuf( myDest );
        }
    }
};

EDIT:

I just read your desired output in a little more detail. The same basic IndentingStreambuf works, but

  1. you'll have to adapt it to support an arbitrary indent (as argument to the constructor),

  2. initialize myIsAtStartOfLine to false, and

  3. call it after having output A(i) = [.

And of course, define operator<< for Matrix appropriately.

Alternatively, you could just use std::ios_base::xalloc to obtain a place where you could tell the operator<< for Matrix about the indent. Something along the lines of:

static long& getMatrixIndent( std::ostream& dest )
{
    static int ourIndex = std::ostream::xalloc();
    return dest.iword( ourIndex );
}

class indent
{
    int myIndent;
public:
    indent( int n ) : myIndent( n ) {}
    friend std::ostream& operator<<( std::ostream& dest,
                                     indent const& manip )
    {
        getMatrixIndent( dest ) = myIndent;
    }
};

std::ostream& operator<<( std::ostream& dest, Matrix const& object )
{
    int indent = getMatrixIndent( dest );
    //  ...
}

This is considerably less flexible than a filtering streambuf. In particular, it can't be made to work for types where you don't control the implementation of operator<<. But it's simple, and also useful at times.

(I'd argue, in fact, that anyone using C++ should be familiar with both techniques. But I'm not aware of any tutorial text which even mentions them. And in a world where a large number of C++ users are still writing while ( ! file.eof() ), it's probably too much to ask for.)

Upvotes: 1

Related Questions