Overview
Having previously written an article on iostream
inserters and extractors, I
decided to write an article on modifiers. Most people use iostream
modifiers without even realising. std::endl
is the most common modifier,
it is simply an item that can be passed into a stream that affects the output.
std::endl
We're going to start by picking on std::endl
. How often do you see code
like this ?
for(int i = 0; i < 100; ++i) myStream << "This is function
number " << lNum << " " << m_szArray[i] << endl;
What is often not understood is that std::endl
does not just pass in a line
break. It also sends a flush
to the stream, which can be a real
performance hit, if done as seen above. Basically, a buffered stream will
unload when flush
is called, or the stream is destroyed. The advantage
of the buffer is almost entirely lost in the above case. So, as a simply
first example, I am going to show how to create a new modifier which does the
line break without the flush. It looks like this:
#include <iosfwd>
class Newline
{
public:
friend std::ostream & operator <<(std::ostream &os, const Newline nl)
{
os << "\r\n";
return os;
}
};
This leaves us able to create a new line like this:
cout << "Here is my line " << Newline();
It works like this. Our code creates a new, unnamed instance of our
class. During it's (brief) lifetime, the stream inserter is called, and
the instance is passed in. Our implementation of a stream operator is
rudimentary, it simply passes in the new line and returns the stream object (as it
should).
Gettin' jiggy with it
The prior example was deliberately simple in order to illustrate the core
functionality which any modifier will need to have. Our next will be a
little more complex: it will accept parameters. In my work I do a lot of
interacting with databases, and I quite often construct queries using an
ostringstream
. Under those circumstances I am quite often querying a
range based on date, and so I wrote a modifier to insert a
datestamp. We will build this modifier now, a step at a time.
Constants
The first thing we will need is values to pass into the constructor. I
have built only four, but it will be easy for you to add more. Some
obvious ones to add would be dd/mm/yyyy and mm/dd/yyyy formats. For my
purposes, we generally want ISO8601 format, with or without the time, and I've
added one which includes the month in a text format, which is the other format
that is universally readable ( i.e. it does not cause confusion as to which
number is the month and which is the day ). After much agonising, I
elected to include this code in namespace std, because while overall I would
not use std as a dumping ground for stuff I wanted to protect with a namespace,
this code does interact directly with the std library and it's not
illogical for it to live with it.
namespace std
{
const int DT_ISO8601 = 1;
const int DT_ISO8601DateOnly = 2;
const int DT_DD_MMM_YYYY = 3;
const int DT_HH_MM = 4;
const std::string DateStamp::Dates [12] =
{
"Jan.", "Feb.", "March", "Apr.", "May", "June",
"July", "Aug", "Sept.", "Oct.", "Nov.", "Dec."
};
The constructor
Our constructor will use the keyword explicit to ensure that no implicit
conversions take place with values passed into it. All parameters have
default values, so it is easy to build our preferred datestamp format.
The parameters are the format constant, an offset ( so you can pass in -7 to
get the date a week ago, or 1 to get tomorrow's date ), and the delimiters used
between date elements and time elements. Note I have used strings instead
of chars so you can have multicharacter delimiters if you want to.
class DateStamp
{
public:
explicit DateStamp(int nType = DT_ISO8601, int nOffset = 0,
std::string sDateDelimiter = "-",
std::string sTimeDelimiter = ":")
: datetype(nType), dateDelimiter(sDateDelimiter),
timeDelimiter(sTimeDelimiter), offset(nOffset) {}
The inserter
The guts of the inserter are reasonable obvious and uninteresting. I
use ::time and ::localtime to get to the point of having a tm struct with the
timestamp in it, then do a switch on the format value in order to figure out
what to stream into a stringstream, which we then dump into the target
stream. Probably the most important thing is to note that it is defined
inside the class, and is defined as a friend of the class. This means we
can make our variables private, and still have access to them within the
inserter.
friend std::ostream & operator <<(std::ostream &os, const DateStamp &mm)
{
time_t lTime; ::time(&lTime);
lTime += mm.offset * 86400;
tm* ptmDate = ::localtime(&lTime);
std::ostringstream ss;
ss.fill('0');
switch(mm.datetype)
{
case DT_ISO8601:
case DT_ISO8601DateOnly:
ss << ptmDate->tm_year + 1900 << mm.dateDelimiter;
ss << std::setw(2) << ptmDate->tm_mon + 1 << mm.dateDelimiter;
ss << std::setw(2) << ptmDate->tm_mday;
if (mm.datetype == DT_ISO8601DateOnly) break;
ss << "T" << std::setw(2) << ptmDate->tm_hour << mm.timeDelimiter;
ss << std::setw(2) << ptmDate->tm_min << mm.timeDelimiter;
ss << std::setw(2) << ptmDate->tm_sec;
break;
case DT_DD_MMM_YYYY:
ss << std::setw(2) << ptmDate->tm_mday << mm.dateDelimiter;
ss << Dates[ptmDate->tm_mon] << mm.dateDelimiter
<< ptmDate->tm_year + 1900;
break;
case DT_HH_MM:
ss << std::setw(2) << ptmDate->tm_hour << mm.timeDelimiter;
ss << std::setw(2) << ptmDate->tm_min;
break;
}
os << ss.str();
return os;
};
Modifiers with state
The date modifiers above all force a month or day less than 10 to have a
preceding 0 by passing in first std::setw(2). How does this work ?
Obviously at the point that the modifier is passed in, iostreams does not yet
know the value it needs to force to this width. The answer is that
modifiers can have state. It is a beautiful thing, and it works like
this. When we write a modifier which will have state, we put a
static int into the class declaration, and we initialise it with a call to
std::ios_base::xalloc()
. This returns an int
, which could well be
different between times that it is run. I believe it is an
index into a linked list ( only because that makes the most sense ), but
whatever it is, it can then be used to set and get either a void *, or an
integer, using either the iword
or pword
functions, which are present
in your stream object passed in, but the values are shared through
iostreams, so if you wanted to set or get a value and did not have a
stream to use, you could just as well use cout
, or any other standard
stream, or even construct one ( although I don't know why you would ). To
illustrate, our final example is a simple class which is designed to hold
someone's name details. The constructor takes two strings, a first name
and a second name. We once again place these in std, although I usually
would put them elsewhere, I don't expect anyone to use this example apart
from for illustrative purposes. We also again set values for the
formats we will be able to use.
namespace std
{
const int NAME_FIRST_LAST = 1;
const int NAME_LAST_FIRST = 2;
const int NAME_INITAL_LAST = 3;
const int NAME_INITIALS = 4;
class CName
{
friend class NameFormat;
public:
CName(std::string first, std::string last)
: m_sFirst(first), m_sLast(last)
{};
Pretty straightforward stuff so far. The next line is where things get
interesting. For the sake of clarity, I have shown the variable inside
the class scope, and then shown how we declare it's value outside it.
class CName
{
...
static int GetAlloc(){return m_nAlloc;};
private:
std::string m_sFirst, m_sLast;
static const int m_nAlloc;
...
}
const int CName::m_nAlloc = std::ios_base::xalloc();
As I've already mentioned, the call to xalloc
returns an index which we will
then use to pass values through iostreams. I say 'through', because the
fact is that our modifier will use this index for the purpose of storing a
value, and the inserter for the class will pull it out and use it to decide how
to format the output. It is therefore a matter of personal taste if you
feel the value belongs in the class or in the modifier. I go for the
class, because the modifier is defined inside the class also. So inside
the class definition, we define our inserter as follows:
friend std::ostream & operator <<(std::ostream &os, const CName &nm)
{
if (!os.good())
return os;
std::ostream::sentry sentry(os);
if(sentry)
{
std::ostringstream ss;
switch(os.iword(nm.m_nAlloc))
{
default:
case NAME_FIRST_LAST:
ss << nm.m_sFirst << " " << nm.m_sLast;
break;
case NAME_LAST_FIRST:
ss << nm.m_sLast << ", " << nm.m_sFirst;
break;
case NAME_INITAL_LAST:
ss << nm.m_sFirst[0] << ". " << nm.m_sLast;
break;
case NAME_INITIALS:
ss << nm.m_sFirst[0] << nm.m_sLast[0];
break;
}
os << ss.str();
}
return os;
};
If you'd like to know more about writing iostream inserters and extractors,
please refer to my article on the subject.
All that remains is to define the stream modifier, which will set the value
stored in iostreams and retrieved by our inserter.
class NameFormat
{
public:
explicit NameFormat(int nFormat) : m_nFormat(nFormat){}
template<class charT class Traits class="",>
friend std::basic_ostream<charT Traits,> & operator <<
(std::basic_ostream<charT Traits ,>& os,
const NameFormat & nf)
{
os.iword(CName::GetAlloc()) = nf.m_nFormat;
return os;
}
private:
int m_nFormat;
};
And there it is. We can now print different formats from the same name,
simply by passing them in through our modifier. Here is the full listing
of the example program:
#include "stdafx.h"
#include
#include "Date Inserter.h"
#include "Newline.h"
#include "Name.h"
using std::cout;
using std::cin;
using std::endl;
using std::CName;
using std::NameFormat;
using std::DateStamp;
int _tmain(int argc, _TCHAR* argv[])
{
cout << "My output " << endl;
cout << "My other output " << Newline();
cout << "And some more.... " << Newline();
CName name("Christian", "Graus");
cout << name << Newline();
cout << NameFormat(std::NAME_LAST_FIRST) << name << Newline();
cout << NameFormat(std::NAME_INITAL_LAST) << name << Newline();
cout << NameFormat(std::NAME_INITIALS) << name << Newline();
cout << DateStamp() << Newline();
cout << DateStamp(std::DT_ISO8601DateOnly) << Newline();
cout << DateStamp(std::DT_DD_MMM_YYYY) << Newline();
cout << DateStamp(std::DT_HH_MM) << Newline();
int i;
cin >> i;
return 0;
}
and the output look like this:
My output
My other output
And some more....
Christian Graus
Graus, Christian
C. Graus
CG
2002-07-15T20:19:59
2002-07-15
15-July-2002
20:19
I hope you agree with me that iostreams is a highly flexible and useful
framework. Add the ability to define your own stream types and you have a
system by which you can easily pass any information you please, where-ever you
choose. My detractors often bring up the fact that iostreams seems to add
80 odd kilobytes to your executable. This is probably true (that is to
say, I have not checked, but I believe the people who have told me).
However, this is hardly a concern to me, because I use iostreams *constantly*,
and so see a lot of benefit for my 80k. In the interest of a balanced
account, however, I mention this so that you are aware of it when deciding if
you will use them on a particular project or not.