Click here to Skip to main content
15,881,381 members
Articles / Programming Languages / C++
Article

IOStream Inserters And Extractors

Rate me:
Please Sign up or sign in to vote.
4.29/5 (9 votes)
16 Apr 20027 min read 83.4K   968   23   4
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


Written By
Software Developer (Senior)
Australia Australia
Programming computers ( self taught ) since about 1984 when I bought my first Apple ][. Was working on a GUI library to interface Win32 to Python, and writing graphics filters in my spare time, and then building n-tiered apps using asp, atl and asp.net in my job at Dytech. After 4 years there, I've started working from home, at first for Code Project and now for a vet telemedicine company. I owned part of a company that sells client education software in the vet market, but we sold that and I worked for the owners for five years before leaving to get away from the travel, and spend more time with my family. I now work for a company here in Hobart, doing all sorts of Microsoft based stuff in C++ and C#, with a lot of T-SQL in the mix.

Comments and Discussions

 
QuestionTypo? Pin
George18-Apr-02 8:59
George18-Apr-02 8:59 
AnswerRe: Typo? Pin
Christian Graus18-Apr-02 9:04
protectorChristian Graus18-Apr-02 9:04 
GeneralRe: Typo? Pin
Tim Smith18-Apr-02 9:55
Tim Smith18-Apr-02 9:55 
GeneralRe: Typo? Pin
Christian Graus18-Apr-02 10:20
protectorChristian Graus18-Apr-02 10:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.