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

iostream modifiers

0.00/5 (No votes)
14 Jul 2002 1  
An exploration of extending the iostreams framework through stream modifiers

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; // 86400 seconds in a day 

    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.

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