Overview
The purpose of this article is to explain how iostream
s 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 iostream
s, 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 ? (iostream
s 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)
{
if (!os.good()) return os;
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 )
{
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 iostream
s 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 iostream
s to read
and write files to disk. I hope you've found this useful, and a
compelling reason to use iostream
s instead of MFC classes like CFile
.