When creating formatted output for a user defined type it is often desirable to define custom formatting flags. For example, it would be nice if a custom string class could optionally add quotes around the string:
String str("example");
std::cout << str << ' ' << squotes << str << << ' ' << dquotes << str << '\n';
should produce
example 'example' "example"
It is easy enough to create manipulators for changing the formatting flags themselves:
std::ostream& squotes(std::ostream& out) {
// what magic goes here?
return out;
}
std::ostream& dquotes(std::ostream& out) {
// similar magic as above
return out;
}
std::ostream& operator<< (std::ostream& out, String const& str) {
char quote = ????;
return quote? out << quote << str.c_str() << quote: str.c_str();
}
... but how can the manipulators store which quotes should be used with the stream and later have the output operator retrieve the value?
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 anint
as key and yields anint&
which starts out as0
.pword()
which takes anint
as key and yields avoid*&
which starts out as0
.xalloc()
astatic
function which yields a differentint
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 newstd::locale
isimbue()
d.For storing simple formatting information as in the
String
example it is sufficient to allocate anint
and store a suitable value in aniword()
:The implementation uses the
stringFormatIndex()
function to make sure that exactly one index is allocate asrc
is initialized the first time the function is called. Sinceiword()
returns0
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 thechar
value of the quote is simply stored in theiword()
.Using
iword()
is rather straight forward because there is isn't any resource management necessary. For the sake of example, let's say theString
should be printed with a string prefix, too: the length of the prefix shouldn't be restricted, i.e., it won't fit into anint
. Setting a prefix is already a bit more involved as a corresponding manipulator needs to be a class type: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 apword()
. Since there can only be avoid*
stored, it is necessary to allocate memory and maintain potentially existing memory: if there is already something stored it must be astd::string
and it is changed to the new prefix. Otherwise, a callback is registered which is used to maintain the content of thepword()
and once the callback is registered a newstd::string
is allocated and stored in thepword()
.The tricky business is the callback: it is called under three conditions:
s
is destroyed ors.copyfmt(other)
is called, each registered callback is called withs
as thestd::ios_base&
argument and with the eventstd::ios_base::erase_event
. The objective with this flag is to release any resources. To avoid accidental double release of the data, thepword()
is set to0
after thestd::string
is deleted.s.copyfmt(other)
is called, the callbacks are called with the eventstd::ios_base::copyfmt_event
after all callbacks and contents was copied. Thepword()
will just contain a shallow copy of the original, however, i.e., the callback needs to make a deep copy of thepword()
. Since the callback was called with anstd::ios_base::erase_event
before there is no need to clean anything up (it would be overwritten at this point anyway).s.imbue()
is called the callbacks are called withstd::ios_base::imbue_event
. The primary use of this call is to updatestd::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 ofiword()
orpword()
which doesn't usexalloc()
there is a chance that indices collide. Thus, it is important to usexalloc()
to make different code play nicely together.Here is a live example.