The streams classes were designed to be extensible, including the ability to store additional information: the stream objects (actually the common base class std::ios_base
) provide a couple of functions managing data associated with a stream:
iword()
which takes an int
as key and yields an int&
which starts out as 0
.
pword()
which takes an int
as key and yields a void*&
which starts out as 0
.
xalloc()
a static
function which yields a different int
on each call to "allocate" a unique key (they keys can't be released).
register_callback()
to register a function which is called when a stream is destroyed, copyfmt()
is called, or a new std::locale
is imbue()
d.
For storing simple formatting information as in the String
example it is sufficient to allocate an int
and store a suitable value in an iword()
:
int stringFormatIndex() {
static int rc = std::ios_base::xalloc();
return rc;
}
std::ostream& squote(std::ostream& out) {
out.iword(stringFormatIndex()) = '\'';
return out;
}
std::ostream& dquote(std::ostream& out) {
out.iword(stringFormatIndex()) = '"';
return out;
}
std::ostream& operator<< (std::ostream& out, String const& str) {
char quote(out.iword(stringFormatIndex()));
return quote? out << quote << str.c_str() << quote: out << str.c_str();
}
The implementation uses the stringFormatIndex()
function to make sure that exactly one index is allocate as rc
is initialized the first time the function is called. Since iword()
returns 0
when there is no value, yet, set for a stream, this value is used for the default formatting (in this case to use no quotes). If a quote should be used the char
value of the quote is simply stored in the iword()
.
Using iword()
is rather straight forward because there is isn't any resource management necessary. For the sake of example, let's say the String
should be printed with a string prefix, too: the length of the prefix shouldn't be restricted, i.e., it won't fit into an int
. Setting a prefix is already a bit more involved as a corresponding manipulator needs to be a class type:
class prefix {
std::string value;
public:
prefix(std::string value): value(value) {}
std::string const& str() const { return this->value; }
static void callback(std::ios_base::event ev, std::ios_base& s, int idx) {
switch (ev) {
case std::ios_base::erase_event: // clean up
delete static_cast<std::string*>(s.pword(idx));
s.pword(idx) = 0;
break;
case std::ios_base::copyfmt_event: // turn shallow copy into a deep copy!
s.pword(idx) = new std::string(*static_cast<std::string*>(s.pword(idx)));
break;
default: // there is nothing to do on imbue_event
break;
}
}
};
std::ostream& operator<< (std::ostream& out, prefix const& p) {
void*& pword(out.pword(stringFormatIndex()));
if (pword) {
*static_cast<std::string*>(pword) = p.str();
}
else {
out.register_callback(&prefix::callback, stringFormatIndex());
pword = new std::string(p.str());
}
return out;
}
To create a manipulator with argument an object is created which captures the std::string
which is to be used as prefix and an "output operator" is implemented to actually set up the prefix in a pword()
. Since there can only be a void*
stored, it is necessary to allocate memory and maintain potentially existing memory: if there is already something stored it must be a std::string
and it is changed to the new prefix. Otherwise, a callback is registered which is used to maintain the content of the pword()
and once the callback is registered a new std::string
is allocated and stored in the pword()
.
The tricky business is the callback: it is called under three conditions:
- When the stream
s
is destroyed or s.copyfmt(other)
is called, each registered callback is called with s
as the std::ios_base&
argument and with the event std::ios_base::erase_event
. The objective with this flag is to release any resources. To avoid accidental double release of the data, the pword()
is set to 0
after the std::string
is deleted.
- When
s.copyfmt(other)
is called, the callbacks are called with the event std::ios_base::copyfmt_event
after all callbacks and contents was copied. The pword()
will just contain a shallow copy of the original, however, i.e., the callback needs to make a deep copy of the pword()
. Since the callback was called with an std::ios_base::erase_event
before there is no need to clean anything up (it would be overwritten at this point anyway).
- After
s.imbue()
is called the callbacks are called with std::ios_base::imbue_event
. The primary use of this call is to update std::locale
specific values which may be cached for the stream. For the prefix maintenance these calls will be ignored.
The above code should be an outline describing how data can be associated with a stream. The approach allows storing arbitrary data and multiple independent data items. It is worth noting that xalloc()
merely returns a sequence of unique integers. If there is a user of iword()
or pword()
which doesn't use xalloc()
there is a chance that indices collide. Thus, it is important to use xalloc()
to make different code play nicely together.
Here is a live example.