When an object of a certain type is written to an ostream, there sometimes is the need to specify a different format, a change of representation, which depends on the state of the ostream rather than on the object that is being written.
Also the builtin types aren't always written in the same way to an ostream. Their format can be manipulated with the so called output stream manipulators; the standard C++ manipulators, defined in iomanip.h, are setbase(), setfill(), setprecision(), setw(), resetiosflags() and setiosflags(). The data describing how a builtin type must be written to an ostream is stored in class ios, a virtual base class of class ostream. Therefore, these manipulators only change how (builtin) types are written to one particular ostream, without influencing how they are written to other ostreams.
The purpose of this chapter is to design an implementation of manipulators for custom classes. §1.2 concentrates on how to find the manipulator data that belongs to a particular ostream, §1.3 describes where to store the manipulator data, while §1.4 deals with the implementation of the manipulators themselves. And finally, examples that clarify how to use the implementation of libcw is given in §1.5.
An unique identifier of an ostream is the pointer to its virtual base class ios. This pointer will always be the same for a given ostream object, because the copy constructor of class ios is private.
When writing a custom class to a given ostream, its manipulator data object belonging to that ostream needs to be identified. Therefore, the ios base class pointer, which is used to identify the ostream, must be stored together with the manipulator data object. For this purpose, libcw defines the template class cmanip_id_tct<CMANIP_DATA>, where CMANIP_DATA is the type of the Custom MANIPulator DATA object.
Template class cmanip_id_tct is defined in <libcw/cmanip.h> as
template<class OMANIP_DATA> class omanip_id_tct { OMANIP_DATA omanip_data; class ios const* id; public: omanip_id_tct(class ios const* iosp) : omanip_data(), id(iosp) { } bool operator!=(class ios const* iosp) const { return id != iosp; } OMANIP_DATA const& get_omanip_data(void) const { return omanip_data; } OMANIP_DATA& get_omanip_data(void) { return omanip_data; } };
where id is used to identify the ostream this object belongs to, through a call to operator!=().
Obviously it is not possible to store extra data in ios for arbitrary custom classes. The manipulator data for a given class must be stored in its own class. For each ostream, exactly one manipulator data object of a given type will be needed.
When writing an object to an ostream, its type and the type of the corresponding manipulator data object will be known. Therefore, the manipulator data object can be stored in its own container, specific to the class that is being written, with the ostream as the search key.
It seems reasonable to use a vector as container, since applications do not usually have a lot of streams that objects of the same class are written to. Since there needs to be exactly one and only one container for each class that is written to an ostream, the most logical thing to do is to declare the container a static vector inside a template function which has as sole parameter the type of the object that is being written. The following template function, get_omanip_data(), returns the requested manipulator data as a function of the ostream passed:
template<class TYPE> inline typename TYPE::omanip_data_ct& get_omanip_data(ostream const& os) { typedef omanip_id_tct<typename TYPE::omanip_data_ct> omanip_id_ct; typedef vector<omanip_id_ct> ids_ct; static ids_ct ids; typename ids_ct::iterator i = find(ids.begin(), ids.end(), &os); if (i == ids.end()) i = ids.insert(ids.end(), omanip_id_ct(&os)); return (*i).get_omanip_data(); }
This function encapsulates the container, only providing access to manipulator data objects through a call to it as a function of the TYPE being written and the ostream that it is being written to.
A typedef in class TYPE, the class being written, defines what is the type of the manipulator data object. If no such an object exists, it is automatically created using the default constructor.
Libcw supports manipulators for custom classes much in the same way as the ordinairy manipulators are defined for the builtin types; however, care had to be taken not to polute the global namespace ::std. The manipulators are therefore static member functions of the class that is being written, and call member functions of the corresponding manipulator data object.
A typical way to use manipulators in class Foobar, for example, is:
cout << Foobar::myManipulator(someParameter)
which causes a call to the method myManipulator(someParameter) of the manipulator data object returned by get_omanip_data<Foobar>(cout).
To ease the use of manipulators, libcw defines several macros. The manipulator function macros come in pairs; one macro defines the static manipulator function (compare with Foobar::myManipulator in the example above), the other defines the operator<< that catches it. Because it is possible that there are two or more manipulators with the same type, it is not possible to combine those two macros. Within a given class (like Foobar in the above example) the type of the manipulators will only depend on the number and type of the parameters that are passed. Therefore the OPERATOR macro needs the types of these parameters to be passed too. The FUNCTION macro needs also the name of the manipulator to be passed. There are macros defined for one and two parameter manipulators; they have names that start with __DEFINE_OMANIP1 and __DEFINE_OMANIP2 respectively. Although it is not allowed to use void as a type in these macros, one can use __DEFINE_OMANIP0 which then will call omanip_data_ct::func(void).
For example, if you want to define two manipulators for class Foobar, setcolor and setindent, that both need one int as parameter, you would define Foobar as follows:
#include <libcw/iomanip.h> class Foobar { ... public: typedef Foobar_manipulator_data_ct omanip_data_ct; __DEFINE_OMANIP1_OPERATOR(Foobar, int) __DEFINE_OMANIP1_FUNCTION(setcolor, int) __DEFINE_OMANIP1_FUNCTION(setindent, int) ... };
where Foobar_manipulator_data_ct is the class of the manipulator data that Foobar uses.
An example of manipulators with zero and two parameters is
class Foobar { ... public: typedef Foobar_manipulator_data_ct omanip_data_ct; __DEFINE_OMANIP0(Foobar, hex) __DEFINE_OMANIP0(Foobar, dec) __DEFINE_OMANIP2_OPERATOR(Foobar, int, char) __DEFINE_OMANIP2_FUNCTION(setfill, int, char) __DEFINE_OMANIP2_FUNCTION(setcolor, int, char) ... };
The corresponding Foobar_manipulator_data_ct would then look like:
class Foobar_manipulator_data_ct { ... public: void hex(void); void dec(void); void setcolor(int); void setindent(int); void setfill(int, char); void setcolor(int, char); };
Note that when you use methods with the same name but different parameters, like setcolor in the above example, then when you forget to declare one of these functions the resulting compilation error is quite confusing! You might want to test it out once so you will recognize the problem later on when it happens by mistake.
Finally, the operator<<() function that will print Foobar will contain code like,
ostream& operator<<(ostream& os, Foobar const& f) { Foobar::omanip_data_ct& manip_data(get_omanip_data<Foobar>(os)); ... return os; }