We will play with std::ostream_iterator by enabling for combination with a new custom class and have a look into its implicit conversion capabilities, which can help us with printing:
- The include files come first and then we declare that we use the std namespace by default:
#include <iostream>
#include <vector>
#include <iterator>
#include <unordered_map>
#include <algorithm>
using namespace std;
- Let's implement a transformation function, which maps numbers to strings. It shall return "one" for the value 1, "two" for the value 2, and so on:
string word_num(int i) {
- We fill a hash map with the mappings we need in order to access them later:
unordered_map<int, string> m {
{1, "one"}, {2, "two"}, {3, "three"},
{4, "four"}, {5, "five"}, //...
};
- Now, we can feed the hash map's find function with the argument, i, and return what it finds. If it doesn't find anything, because there is no translation for a given number, we return the string, "unknown":
const auto match (m.find(i));
if (match == end(m)) { return "unknown"; }
return match->second;
};
- Another thing with which we will play later with is struct bork. It only contains an integer and is also implicitly constructible from an integer. It has a print function, which accepts an output stream reference and prints the "bork" string repeatedly, depending on the value of its member integer borks:
struct bork {
int borks;
bork(int i) : borks{i} {}
void print(ostream& os) const {
fill_n(ostream_iterator<string>{os, " "},
borks, "bork!"s);
}
};
- In order to gain convenience with bork::print we overload operator<< for stream objects, so they automatically call bork::print whenever bork objects are streamed into an output stream:
ostream& operator<<(ostream &os, const bork &b) {
b.print(os);
return os;
}
- Now we can finally begin implementing the actual main function. We initially just create a vector with some example values:
int main()
{
const vector<int> v {1, 2, 3, 4, 5};
- Objects of type ostream_iterator need a template parameter, which denotes which type of variables they can print. If we write ostream_iterator<T>, it will later use ostream& operator(ostream&, const T&) for printing. This is exactly what we implemented before for the bork type, for example. This time, we are just printing integers, so it is ostream_iterator<int>. It shall use cout for printing, so we provide it as the constructor parameter. We go through our vector in a loop and assign each item i to the dereferenced output iterator. This is how stream iterators are used by STL algorithms too:
ostream_iterator<int> oit {cout};
for (int i : v) { *oit = i; }
cout << 'n';
- The output of the iterator we just produced is fine, but it prints the number without any separator. If we want a bit of separating whitespace between all printed items, we can provide a custom spacing string as a second parameter of the output stream iterator's constructor. This way, it prints "1, 2, 3, 4, 5, " instead of "12345". Unfortunately, we cannot easily tell it to drop the comma-space string after the last number, because the iterator does not know of its end before it reaches it:
ostream_iterator<int> oit_comma {cout, ", "};
for (int i : v) { *oit_comma = i; }
cout << 'n';
- Assigning items to an output stream iterator in order to print them is not a wrong way to use it, but this is not what they were invented for. The idea is to use them in combination with algorithms. The simplest one is std::copy. We can provide the begin and end iterators of the vector as an input range and the output stream iterator as the output iterator. It will print all the numbers of the vector. Let's do that with both the output iterators and later compare the output with the loops we wrote before:
copy(begin(v), end(v), oit);
cout << 'n';
copy(begin(v), end(v), oit_comma);
cout << 'n';
- Remember the function, word_num, which maps numbers to strings, as 1 to "one", 2 to "two", and so on? Yes, we can use those for printing too. We just need to use an output stream operator, which is template specialized on string because we are not printing integers any longer. And instead of std::copy, we use std::transform because it allows us to apply a transformation function to each item in the input range before copying it to the output range:
transform(begin(v), end(v),
ostream_iterator<string>{cout, " "},
word_num);
cout << 'n';
- The last output line in this program finally puts struct bork to use. We could, but do not provide a transformation function to std::transform. Instead, we can just create an output stream iterator, which is specialized on the bork type in an std::copy call. This leads to the bork instances being implicitly created from the input range integers. That will give us some interesting output:
copy(begin(v), end(v),
ostream_iterator<bork>{cout, "n"});
}
- Compiling and running the program yields us the following output. The first two lines are completely identical to the next two lines, which is what we suspected. Then, we get nice, written-out number strings in a line, followed by a lot of bork! strings. These occur in multiple lines because we used a "n" separator string instead of spaces for those:
$ ./ostream_printing
12345
1, 2, 3, 4, 5,
12345
1, 2, 3, 4, 5,
one two three four five
bork!
bork! bork!
bork! bork! bork!
bork! bork! bork! bork!
bork! bork! bork! bork! bork!