Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

IOStream Inserters And Extractors

0.00/5 (No votes)
16 Apr 2002 1  
Showing how to extend iostreams in order to stream custom types

Sample Image

Overview

The purpose of this article is to explain how iostreams can be extended to support custom types. The iostream library is the C++ standard library's facility for streaming data to and from sources such as files (fstream), strings (sstream) and the console (cin/cout). The canonical 'Hello World' program using iostreams would look like this:

#include<iostream>

        
using std::cout;
using std::endl;

int main()
{
    cout << "Hello World" << endl;
}

Using iostreams, we stream values from our variables into an iostream using the << operator, and we extract them using the >> operator. Some stream types are bi-directional, but we will use all unidirectional types in this article, i.e. I prefer to use ifstream and ofstream instead of fstream. This is simply because I never need a bidirectional facility, and more options are required to set up a bidirectional stream, the unidirectional file streams all have meaningful default parameter values. I should note that while the core code presented here (in stdafx.h) will work on any standards conforming C++ compiler (it will also work with Visual C++), the project was written using VS.NET, so you will have to create your own project if you're using a different compiler.

Inserters and extractors

Support a type involves two steps, each of which can be done without the other, depending on what is needed. An extractor defines operator >> and and inserter defines operator << for a given type. As I said, we can choose to define one and not the other, which we have in fact done for CString, where we define an inserter only.

Defining an inserter

In order to define an inserter we use the following prototype:

template<class charT, class Traits>
std::basic_ostream<charT, Traits> &
operator << (std::basic_ostream<charT, Traits> & os, const TYPE & t)

The type needs to be defined const, so that we can pass const types into it. It is in any case wise to use const wherever you can, and we certainly do not want to modify the value of our parameter.

We are passed the stream and the variable, we need also to return the stream. This is because stream values can be concatenated, like this:

cout << "The number of " << strType[i] " 
     << " it takes to change a lightbulb is " << nNumber[i]<< " because " 
     << strReason[i] << endl;

For this reason the stream object must be returned to be passed into the next operator <<.

A first attempt

The most obvious thing then to do in this case would be something like:

template<class charT, class Traits>
std::basic_ostream<charT, Traits> &
operator << (std::basic_ostream<charT, Traits> & os, const POINT & pt)
{
    os << "x: " << pt.x;
    os << " y: " << pt.y;
    return os;
}

This will indeed stream the point value so that we get something like "x: 0 y: 48", but it will only work in certain circumstances.

Checking the stream state

First of all, it will only work if the stream coming in is valid. Seems pretty obvious, but wouldn't it be better to check first, and just pass the stream along if it is not valid ? (iostreams have an exception mechanism whereby if a person using a stream wanted an exception raised when a particular type of error occurred, then the code would not get to the point of our inserter if the stream was broken.)

The stream has an isgood() method which performs this test for us, like so:

if (!os.good()) return os;

The next problem is that streams can define pre and post fix operations, i.e. actions which should occur before and after each insertion. These can be added to also. These are handled by the sentry object, a class which needs to be instantiated prior to our insertion, the constructor performs our prefix operations and the destructor handles the postfix operations.

typename std::basic_ostream<chart Traits,>::sentry opfx(os);

This object also defines operator bool to allow easy checking for success, so we will check this prior to our operation.

Maintaining stream integrity

The main problem is a bit more serious. As covered in my ostringstream article, the fact is that there are a number of modifiers that can be passed into a stream, which either need to be applied to the operation as a single action ( for example alignment ) or which apply only to the next insertion to the stream and are then reset. I've seen a number of lengthy solutions to this problem, but my solution is quite simple. Use ostringstream to build the item to be inserted, then insert it into the stream as a single string, thus causing all formatting etc. to work perfectly by default.

The end result, which shows this operation and all the prior mentioned changes, looks like this:

template<class charT, class Traits>
std::basic_ostream<charT, Traits> &
operator << (std::basic_ostream<charT, Traits> & os, 
            const POINT & pt)
{
    //Check stream state first


    if (!os.good()) return os;

    // Create sentry for prefix operations ( it's destructor will 

    // carry out postfix operations )


    typename std::basic_ostream<charT, Traits>::sentry opfx(os);

    if (opfx)
    {
        std::ostringstream str;
        str << "x: " << pt.x;
        str << " y: " << pt.y;
        os << str.str().c_str();
    }

    return os;
}

Defining an extractor

The prototype for an extractor is not that different - the main thing to note is that our object is no longer const, as we intend to fill it with values from our stream.

template<class charT, class Traits>
std::basic_istream<charT, Traits> &
operator >> (std::basic_istream<charT, Traits> & is, TYPE & T)

Apart from that, our strategy is similar, the difference is that because of the formatting we provided, the stream contains information we wish to discard. We use a std::string to hold our data, and we call the >> operator (which is delimited in it's operation by spaces as well as newlines), and when we've read a value we want, we use atoi to turn it into a digit.

template<class charT, class Traits>
std::basic_istream<charT, Traits> &
operator >> (std::basic_istream<charT, Traits> & is, POINT & pt)
{
    if (!is.good()) return is;
    typename std::basic_istream<charT, Traits>::sentry opfx(is);

    if (opfx)
    {
        std::string s;
        is >> s;
        is >> s;
        pt.x = atoi(s.c_str());
        is >> s;
        is >> s;
        pt.y = atoi(s.c_str());
    }

    return is;
}

The sample program

The sample program uses a doc/view MFC program, with an edit view. When the view is moved, or we move the mouse in the view with the button down, we stream the co-ordinates of the window or the mouse and pass them to a function which outputs them to the view:

void CMainFrame::OnMove(int x, int y)
{
    if (::IsWindowVisible(m_hWnd))
    {
        CRect rc;
        GetWindowRect(&rc);

        ostringstream strm;
        strm << rc;

        CIOStreamInsertersView * pView
             = dynamic_cast<CIOStreamInsertersView *>(GetActiveView());

        ASSERT(pView);
    
        pView->InsertString(s);
    }
}

void CIOStreamInsertersView::OnMouseMove(UINT nFlags, CPoint point)
{
    if (::GetAsyncKeyState(VK_LBUTTON) && ::GetAsyncKeyState(VK_LBUTTON))
    {
        ostringstream strm;
        strm << point;

        InsertString(strm.str().c_str());
    }

    CRichEditView::OnMouseMove(nFlags, point);
}

As an aside, the reason I call GetAsyncKeystate twice is that it returns if the key was pressed since the last time you checked. Someone at work who did not know this once had a most amusing bug where a key only worked every second time he pressed it - you have been warned.

Adding a string to the end of a CEditView

Both these functions call InsertString, which looks like this:

void CIOStreamInsertersView::InsertString (CString s, bool bAdd /* = true */)
{
    // Add string to document object

    if (bAdd)
        GetDocument()->m_vecDocument.push_back(s);

    s += "\r\n";

    m_bOverflow = false;

    int nLength = (int) SendMessage(WM_GETTEXTLENGTH, 0, 0);

    SendMessage(EM_SETSEL, nLength, nLength);

    SendMessage(EM_REPLACESEL, 0, (LPARAM)s.GetBuffer(s.GetLength()));
    s.ReleaseBuffer();

    if (m_bOverflow)
    {
        SendMessage(EM_SETSEL, 0, s.GetLength());
        char empty = 0;
        SendMessage(EM_REPLACESEL, 0, (LPARAM)&empty);

        nLength = (int) SendMessage(WM_GETTEXTLENGTH, 0, 0);

        SendMessage(EM_SETSEL, nLength, nLength);

        SendMessage(EM_REPLACESEL, 0, (LPARAM)s.GetBuffer(s.GetLength()));
        s.ReleaseBuffer();
    }
}

bAdd is a flag we set to false when reading a file, so that our vector is not filled again (which has dire consequences as it invalidates the iterators we are stepping through at the time). The vector itself contains the strings we have on display. This is a terrible design because our display is not attached to the data we load/save, but fixing it would hardly enhance the sample, so I did it in a way that emphasised using the new operators more so than a robust design.

The sequence to insert a string at the end of an edit view is to call WM_GETTEXTLENGTH in order to find out how many characters are in the view, EM_SETSEL to set the cursor to select only the location at the end of the file, then call EM_REPLACESEL to replace that end position with the string in question. We also need to handle EN_MAXTEXT, which will be called if the string we try to insert causes an overflow.

void CIOStreamInsertersView::OnEnMaxtext() 
{ 
    m_bOverflow = true; 
}

We set our member bool in EN_MAXTEXT. (It could as easily be a static because it gets set to false at the start and we are testing simply to see if OnEnMaxText has been called or not.) If it was called, we select a string from the top of the view to the length of the string, replace it with an empty string and then we insert our string at the bottom again, having made enough room to insert it.

Serialisation

For the sake of the example, I've overloaded OnFileLoad and OnFileSave, and I've not bothered to add a file select dialog, just a hard coded path. The load function creates an ifstream and then reads in the file, using values of 1 and 2 to tell if a value is a RECT or a POINT. Then the RECT and POINT values are read in, converted back to strings and passed to the view, using the false flag so that our vector is not altered, otherwise the insert would invalidate the iterators we are stepping through and it would blow up. The load function looks like this:

void CIOStreamInsertersDoc::OnFileOpen()
{
    ifstream file("iostream test.txt");

    m_vecDocument.clear();
    std::string s;

    RECT rc;
    POINT pt;

    while (!file.eof())
    {
        file >> s;

        ostringstream str;
        
        if ('1' == s[0]) 
        {
            file >> pt;
            str << pt;
            CString sData(str.str().c_str());
            m_vecDocument.push_back(sData);
        }
        else if ('2' == s[0])
        {
            file >> rc;
            str << rc;
            CString sData(str.str().c_str());
            m_vecDocument.push_back(sData);
        }
    }

    CIOStreamInsertersView * pView
         = dynamic_cast<CIOStreamInsertersView *>
                       (dynamic_cast<CMainFrame*>
                         (AfxGetMainWnd())->GetActiveView());

    ASSERT(pView);

    pView->SetWindowText("");

    std::vector<CString>::iterator it = m_vecDocument.begin();
    std::vector<CString>::iterator end = m_vecDocument.end();

    for (;it != end; ++it)
    {
        CString s(*it);
        pView->InsertString(*it, false);
    }
}

Obviously, it would be much easier just to read in the strings, but that defeats the purpose of the example, now doesn't it ?

OnFileSave

The save function pretty much unwinds exactly the way the load wound up. It steps through the vector and writes out the delimiting 1 or 2 followed by the string in the vector.

void CIOStreamInsertersDoc::OnFileSave()
{
    ofstream file("iostream test.txt");

    std::vector<CString>::iterator it = m_vecDocument.begin();
    std::vector<CString>::iterator end = m_vecDocument.end();

    CString strTemp;

    for (;it != end; ++it)
    {
        strTemp = *it;

        if ("x" == strTemp.Left(1))
            file << 1 << endl;
        else
            file << 2 << endl;

        file << strTemp << endl;
    }

    file.close();
}

Summary

The point of this article has been to show the ways in which iostreams can be extended to handle custom types. We have handled POINT and RECT ( which by default cover CPoint and CRect as well ), but through the article it should be clear how to proceed to provide handlers for any other types, including your own classes, which you may want to pass into a stream for some reason. I've also covered how to pass a string to the end of a CEditView, and a number of other topics in passing, and shown by example how to use iostreams to read and write files to disk. I hope you've found this useful, and a compelling reason to use iostreams instead of MFC classes like CFile.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here