- Built-in Manipulators
- Custom Manipulators for User-Defined Objects
- User-Defined Inserters and Extractors
Custom Manipulators for User-Defined Objects
The built-in manipulators go a long way, but we need more. In Listing 1, we used built-in manipulators to format a single attribute of our user-defined course object. While this is useful, we would also like to insert our entire course object into a stream and have it properly formatted. For instance, sometimes our course object needs to be represented as XML, sometimes HTML, sometimes as a horn clause. Further, various data members need to be represented differently depending on the stream. Our course start times and end times could be represented using military time, simple (wall clock) time, etc. While we could write special member functions for each of these activities, it would be nice to simply insert manipulators to do the job.
The advantage of using manipulators, inserters, and extractors over regular member functions for a class is that the stream-and-file metaphor are maintained. In the long run, this makes for code that's easier to read, maintain, and use. The supplier (writer) of a class has to do a little more work up front, but ultimately the supplier's job is made easier and the user of a class benefits immediately.
We want to specify our own manipulators to accommodate user-defined objects. There are two basic kinds of manipulators: those that don't take arguments, such as endl and ends; and those that do, such as setprecision() and setfill(). Manipulators that don't require arguments are easier to implement, taking the following general form:
Xstream &manipulator_name(Xstream &Stream) { // user code // alter the stream in some way // set the stream state return(Stream); }
where Xstream is either a member of the ostream family of classes or from the istream family of classes. For instance, if we wanted to define a manipulator that would change the state of the stream to format floating-point numbers with a decimal point and a precision of 2, we could define a manipulator with the name scientific and use it as shown in Listing 2:
Listing 2 Defining a Scientific Manipulator Using the Built-in Manipulators setiosflags and setprecision to Format a Floating-Point Number
#include <iostream> #include <iomanip> ostream &scientific(ostream &Out) { Out << setiosflags(ios::showpoint | ios::scientific); Out << setprecision(2); return(Out) } int main(int argc,char *argv[]) { ... float Num = 3452.2334; //will insert 3.45e+03 into the stream cout << scientific << Num << endl; ... }
Our scientific manipulator simply ends up being a shortcut for some of the built-in manipulators. We're not always so lucky.
The manipulators that take an argument require two basic functions: the first function defines the manipulator, and the second function is a call to a template that has been defined in iomanip. Which template to use depends on whether the user-defined manipulator will be used with istream, ostream, or a combination of the two:
imanip is used for istream and its derived classes.
omanip is used for ostream and its derived classes.
smanip is used for the combined istream and ostream classes.
Listing 3 defines a manipulator that requires an argument. The manipulator is used to insert spaces into the stream.
Listing 3 Defining a Manipulator That Requires the Number of Spaces To Insert into a Stream as the Argument
#include <fstream> #include <iomanip> ostream &setspaces(ostream &Out, int NumSpaces) { int CurrentWidth; CurrentWidth = Out.width(); Out << setw(NumSpaces) << " "; Out << setw(CurrentWidth); return(Out); } omanip<int> spaces(int NumSpaces) { return omanip<int> (&setspaces, NumSpaces); } int main(int argc,char *argv[]) { ... course Course; cout << Course.startTime() << spaces(11) Course.endTime() << spaces(4) Course.description() << endl; ... }
The setspaces() function in Listing 3 actually does the work of the manipulator, and the spaces() function is used as the name of the manipulator. Again, our spaces() manipulator is a simple shortcut for built-in manipulators; we're inserting our course object one data member at a time into the stream.
We want to insert our entire user-defined course object into the stream and have the appropriate representation used, as well as appropriate formatting for each data member. To do this, we want to use user-defined manipulators so that we can set the stream to a user-defined state. User-defined manipulators often require the use of user-defined format flags; the stream classes have a storage area for such flags. This storage is dynamically allocated for the stream classes. This storage area is accessed by the iword(), pword(), and xalloc() functions:
The xalloc() function returns an index to the next unused flag. The flag is represented by a long. Each time xalloc() is called, a new index is returned.
The iword() and pword() methods are used to set the value or query the value of the flag.
In our case, the user-defined state will determine whether course times are formatted using a military format or a simple (wall clock) format. The user-defined state will determine whether our course object should be formatted as horn clause, XML, or HTML. We use the iword() and xalloc() methods to set up a couple of user-defined state flags. One flag will be used to identify what time format should be used, the other will be used to identify whether horn clause, XML, or HTML will be used. Both the manipulator and the user-defined inserters and extractors will need access to the user-defined flags. There are a couple of easy ways to employ the user-defined format flags so that they can be accessible by the manipulators, inserters, and extractor:
Save the flags in some globally declared container (or appropriate scope).
Derive a new istream or ostream class and make the flags data members of the new class.
We'll use the first technique here. Because every call to xalloc() returns a new index, we want to save that index in a container. The map class is useful because we can associate a flag name with each index:
map<string,long> UserFlag;
We'll set up five simple flags:
const long WallClockTime = 4; const long MilitaryTime = 3; const long HClause = 5; const long HtmlRep = 6; const long XmlRep = 7;
Each one represents a translation state for the stream. In Listing 4, we define two manipulators, one named militaryTime and the other clockTime, and set up our TimeFormat user-defined flag:
Listing 4 Defining Manipulators for Time Formatting and Setting Up a User-Defined Flag
ostream &militaryTime(ostream &SomeStream) { if(UserFlag.find("TimeFormat") == UserFlag.end()){ UserFlag["TimeFormat"] = SomeStream.xalloc(); } SomeStream.iword(UserFlag["TimeFormat"]) = MilitaryTime ; return(SomeStream); } ostream &clockTime(ostream &SomeStream) { if(UserFlag.find("TimeFormat") == UserFlag.end()){ UserFlag["TimeFormat"] = SomeStream.xalloc(); } SomeStream.iword(UserFlag["TimeFormat"]) = WallClockTime ; return(SomeStream); }
These manipulators use the find() method to check whether TimeFormat has already been stored in the map. If not, xalloc() is called to get the next available index from SomeStream; that index is then used for the TimeFormat flag. Each manipulator in Listing 4 sets the value of the TimeFormat flag to the appropriate value:
The militaryTime manipulator sets the TimeFormat flag to MilitaryTime.
The clockTime manipulator sets the TimeFormat flag to WallClockTime.