Tuesday, January 18, 2011

C++: Stream iterators and containers

I've been writing C++ code for several years now (since 2005), but I haven't really been writing idiomatic C++; it always looked more like C code with a personality disorder. Once I saw examples of how the standard containers were used in working code, I glommed onto the pattern pretty quickly; after all, code like

for (std::vector<Foo>::iterator it = foo.begin(); it != foo.end(); ++it)
do_something_with(*it);

was pretty easy to grasp. I understood that iterators were an abstraction for pointers, yeah, but I never really grasped how fundamental they were to writing C++.

Several months ago, I had a neurological event mental breakthrough, and it was inspired by stream iterators, something I had never heard of until recently. You could use an iterator to iterate over an input stream as though it were like any other container. Furthermore, you could use algorithms such as std::copy to load the contents of a stream into a container, like so:

std::copy(std::istream_iterator(in),
std::istream_iterator(),
std::inserter(container, container.end()));


So I womped up this little example that reads a sequence of id-name pairs into a std::map container, then writes the contents of the container back to standard output.


#include <iostream>
#include <iterator>
#include <algorithm>
#include <sstream>
#include <string>
#include <map>

/**
* Define a record type for the istream template parameter.
*/
struct Record
{
int m_id;
std::string m_name;

Record() {}
Record(int id, std::string name) : m_id(id), m_name(name) {}
Record(const Record& rec) : m_id(rec.m_id), m_name(rec.m_name) {}

/**
* Cast operator; converts contents of this record to a std::pair
* suitable for inserting into a map container
*/
operator std::pair<const int, std::string>() const { return std::make_pair(m_id, m_name); }
};

/**
* Converts a map entry to a formatted string
*/
std::string recordToString(std::pair<const int, std::string> it)
{
std::stringstream tmp;
tmp << "{ id: " << it->first << "; name: " << it->second << "}";
return str;
}

/**
* Main function
*/
int main(void)
{
/**
* Set up the input stream. For this example we'll use a string stream for
* input.
*/
std::string data = "1 foo 2 bar 3 bletch 4 blurga";
std::stringstream instream;
instream.str(data);

/**
* Target map container
*/
std::map<int, std::string> vmap;

/**
* Copy data from the stream into the container.
*/
std::copy(std::istream_iterator<Record>(instream), // initially points to beginning of stream
std::istream_iterator<Record>(), // points "nowhere"
std::inserter(vmap, vmap.end())); // creates an output iterator
// pointing to the end of the
// map

/**
* Write the contents of the container back out to standard output. Since
* std::ostream_iterator cannot directly format parameters of type
* std::pair<U,V>, we use the std::transform algorithm to call the
* recordToString function on each element of the map and pass the resulting
* string to the output iterator for standard output.
*/
std::transform(vmap.begin(),
vmap.end(),
std::ostream_iterator<std::string>(std::cout, " "),
recordToString);
std::cout << std::endl;

return 0;
}