Search code examples
c++templatesoperator-overloadingoperatorsstringstream

Templated operator<< isn't being recognized


I created a class called SkipToChar that's supposed to be able to be used as follows:

std::ostringstream oss;
oss << "Hello," << SkipToChar(7) << "world!" << std::endl;

Which would print "Hello, world!" (notice the space.) Basically it's supposed to skip to the character at the specified index using spaces. But apparently the compiler doesn't recognize the operator<< I created for it. Interestingly, calling operator<< explicitly, even without giving any template parameters (like operator<<(oss, SkipToChar(7)); works fine; it just doesn't work if I actual

Here's my code:

#include <iostream>
#include <sstream>

template <typename _Elem>
struct basic_SkipToChar
{
    typename std::basic_string<_Elem>::size_type pos;
    basic_SkipToChar(typename std::basic_string<_Elem>::size_type position)
    {
        pos = position;
    }
};

template <typename _Elem>
inline std::basic_ostringstream<_Elem> &operator<<(std::basic_ostringstream<_Elem> &oss, const basic_SkipToChar<_Elem> &skip)
{
    typename std::basic_string<_Elem>::size_type length = oss.str().length();
    for (typename std::basic_string<_Elem>::size_type i = length; i < skip.pos; i++) {
        oss << (_Elem)' ';
    }
    return oss;
}

typedef basic_SkipToChar<char> SkipToChar;
typedef basic_SkipToChar<wchar_t> WSkipToChar;

int main(int argc, char *argv[])
{
    std::ostringstream oss;
    /*ERROR*/ oss << "Hello," << SkipToChar(8) << "world!" << std::endl;
    std::cout << oss.str();
    return 0;
}

It gives me the following error when I try to compile it:

error C2679: binary '<<' : no operator found which takes a right-hand operand of type 'basic_SkipToChar<char>' (or there is no acceptable conversion)

I marked the line the error is on with a comment. What's going wrong here?


Solution

  • As Jarod42 points out, the return type of all of the built-in operator<< is std::ostream&. This is a basic principle of how streams operator; except for the actual sink or source, the stream should be indifferent as to where the data is going to or coming from.

    It's possible to provide manipulators which only work for one type of stream, or do different things for different types of streams, by using a dynamic_cast in the manipulator:

    std::ostream&
    operator<<( std::ostream& dest, SkipToChar const& manip )
    {
        std::ostringstream* d = dynamic_cast<std::ostringstream*>( &dest );
        if ( d != nullptr ) {
            //  ...
        }
        return dest;
    }
    

    In general, however, this is a very bad idea; clients will be very surprised that outputting to a string results in different text than outputting to a file.

    What you seem to be trying to do is to more or less emulate a form of tabbing. The best way to do this is to use a filtering output streambuf, which keeps track of where you are in the line, and can be inserted between the std::ostream and the actual sink regardless of the type of stream:

    class TabbingOutputStreambuf : public std::streambuf
    {
        std::streambuf* myDest;
        std::ostream*   myOwner;
        int             myInLineCount;
    public:
        TabbingOutputStreambuf( std::streambuf* dest )
            : myDest( dest )
            , myOwner( nullptr )
            , myInLineCount( 0 )
        {
        }
        TabbingOutputStreambuf( std::ostream& dest )
            : myDest( dest.rdbuf() )
            , myOwner( &dest )
            , myInLineCount( 0 )
        {
            myOwner.rdbuf( this );
        }
        ~TabbingOutputStreambuf()
        {
            if ( myOwner != nullptr ) {
                myOwner->rdbuf( myDest );
            }
        }
    
        int overflow( int ch ) override
        {
            if ( ch == '\n' ) {
                myInLineCount = 0;
            } else {
                ++ myInLineCount;
            }
            return myDest->sputc( ch );
        }
    
        //      Special function...
        int tabTo( int n )
        {
            int retval = 0;
            while ( retval == 0 && myInLineCount < n ) {
                if ( overflow( ' ' ) == EOF ) {
                    retval = EOF;
                }
            }
            return retval;
        }
    };
    

    Your manipulator would then be:

    std::ostream&
    operator<<( std::ostream& dest, SkipToChar const& manip )
    {
        TabbingOutputStreambuf* sb = dynamic_cast<TabbingOutputStreambuf*>( dest.rdbuf() );
        assert( sb != nullptr );
        try {
            if ( sb->tabTo( manip.pos ) == EOF ) {
                dest.setstate( std::badbit );
            }
        } catch ( ... ) {
            dest.setstate( std::badbit );
        }
        return dest;
    }
    

    This is still not ideal, since it will fail with an unprotected buffer, but you'd only use the manipulator in a context like:

    void
    generateOutput( std::ostream& dest )
    {
        TabbingOutputStreambuf tabber( dest );
        dest << "Hello, " << SkipToChar( 7 ) << "world!";
        //  ...
    }
    

    And this will work regardless of the type of stream passed to the function (including custom ostream classes that you don't even know about).

    EDIT:

    One last point: don't bother with making things templates until you've got the basic version working. (For what it's worth, your code doesn't work correctly for wchar_t streams anyway. To output a space in them, you need to get the embedded locale, get the ctype facet from it, and use its widen member function.)