How to add indention to the stream operator

2019-03-18 12:52发布

问题:

In our project we use the c++ stream operator (<<) in our object model to print out an easy readible format of the data. A simplified example is this:

std::ostream& operator<<(std::ostream & oStream, const OwnClass& iOwnClass) {
    oStream << "[SomeMember1: " << iOwnClass._ownMember1 << "]\n";
    oStream << "[SomeMember2: " << iOwnClass._ownMember2 << "]\n";
}

Resulting in this in the logging:

[SomeMember1: foo]
[SomeMember2: bar]

What we want now is to be able to indent the result of that operator. Some calling class might not want the result like this, but want to add 2 spaces indention before each line. We could add a member to our class specifying the indention, but that does not seem to be an elegant solution.

Of course this is not a very big issue, but our logging would be so much nicer if this would work.

Thanks

回答1:

The simplest solution is to slip a filtering streambuf between the ostream and the actual streambuf. Something like:

class IndentingOStreambuf : public std::streambuf
{
    std::streambuf*     myDest;
    bool                myIsAtStartOfLine;
    std::string         myIndent;
    std::ostream*       myOwner;
protected:
    virtual int         overflow( int ch )
    {
        if ( myIsAtStartOfLine && ch != '\n' ) {
            myDest->sputn( myIndent.data(), myIndent.size() );
        }
        myIsAtStartOfLine = ch == '\n';
        return myDest->sputc( ch );
    }
public:
    explicit            IndentingOStreambuf( 
                            std::streambuf* dest, int indent = 4 )
        : myDest( dest )
        , myIsAtStartOfLine( true )
        , myIndent( indent, ' ' )
        , myOwner( NULL )
    {
    }
    explicit            IndentingOStreambuf(
                            std::ostream& dest, int indent = 4 )
        : myDest( dest.rdbuf() )
        , myIsAtStartOfLine( true )
        , myIndent( indent, ' ' )
        , myOwner( &dest )
    {
        myOwner->rdbuf( this );
    }
    virtual             ~IndentingOStreambuf()
    {
        if ( myOwner != NULL ) {
            myOwner->rdbuf( myDest );
        }
    }
};

To insert, just create an instance of the streambuf:

IndentingOStreambuf indent( std::cout );
//  Indented output...

When indent goes out of scope, everything returns to normal.

(For logging, I have one that is a bit more complex: the LoggingOStreambuf takes __FILE__ and __LINE__ as arguments, sets myIndent to a formatted string with these arguments, plus a time stamp, resets it to an indentation string after each output, collects all of the output in an std::ostringstream, and outputs it atomically to myDest in the destructor.)



回答2:

This can be done using a custom stream-manipulator that stores the desired indentation-level in a word of the internal extensible array of the stream. You can request such a word using the function ios_base::xalloc. This function will give you the index of your word. You can access it using ios_base::iword. One way to implement that would be this:

struct indent {
    indent(int level) : level(level) {}
private:
    friend std::ostream& operator<<(std::ostream& stream, const indent& val);

    int level;
};

std::ostream& operator<<(std::ostream& stream, const indent& val) {
    for(int i = 0; i < val.level; i++) {
        stream << " ";
    }
    return stream;
}

std::ostream& operator<<(std::ostream & oStream, const OwnClass& iOwnClass) {
    oStream << indent(oStream.iword(index)) << "[SomeMember1: " << 
               iOwnClass._ownMember1 << "]\n";
    oStream << indent(oStream.iword(index)) << "[SomeMember2: " << 
               iOwnClass._ownMember2 << "]\n";
}

You'd have to figure out where to store the index. This effectively allows you to add custom state to the stream (note that this would not be thread-safe out-of-the-box). Every function that wants indentation should add the requested indentation to the stream, and subtract it again when it is done. You could make sure this always happen by using a guard to add/subtract the desired indent (IMHO this is more elegant than using a manipulator):

class indent_guard {
public:
    indent_guard(int level, std::ostream& stream, int index) 
    : level(level),
      stream(stream),
      index(index)
    {
        stream.iword(index) += level;
    }

    ~indent_guard() {
        stream.iword(index) -= level;
    }

 private:
     int level;
     std::ostream& stream;
     int index;
};

You could use it like this:

void some_func() {
    indent_guard(2, std::cout, index);

    // all output inside this function will be indented by 2 spaces

    some_func(); // recursive call - output will be indented by 4 spaces

    // here it will be 2 spaces again
}


回答3:

Not so good way to do this is to add a global variable, which tells the indentation. Something like this :

std::string OwnClassIndentation;
std::ostream& operator<<(std::ostream & oStream, const OwnClass& iOwnClass) {
    oStream << "[SomeMember1:" << OwnClassIndentation << iOwnClass._ownMember1 << "]\n";
    oStream << "[SomeMember2:" << OwnClassIndentation << iOwnClass._ownMember2 << "]\n";
}

And then set it as appropriate.



回答4:

You can make your own stream class that has an indentation variable and override the endl for that class, inserting the indentation.