Streaming Your Own Classes - Reading and Writing Files - C++ All-in-One For Dummies (2009)

C++ All-in-One For Dummies (2009)

Book V

Reading and Writing Files

Chapter 5: Streaming Your Own Classes

In This Chapter

Streaming a class to a text file

Getting the most out of manipulators

Writing your own manipulators

The C++ stream classes can read and write all sorts of goodies, such as integers, characters, strings, floating-point numbers, and Boolean variables. But sooner or later, being able to stream one of your own classes (like the following) would be nice:

MyClass x;

cout << x << endl;

Now C++ has a good reason not to have done this already: The compiler and library can’t know how to stream your class. What should cout write? The name of the class followed by the values of the public member variables? Or maybe just the private member variables? None of the above?

Therefore, you should make the class streamable. In this chapter, we show you how to do it. But recognizing that you have two separate reasons why you may want to make a class streamable is important:

♦ To provide a format for writing the object to a text stream.

♦ To save the information in an object so you can read it back in at a later date, thereby reconstructing the object. A class with this feature is called a persistent class.

We cover both topics in this chapter. We also show how you can create your own manipulators. Remember, a manipulator is this kind of code:

cout << endl;

That is, the endl is the manipulator. You can make your own manipulators that manipulate the stream in various ways, as we show you later in this chapter.

Streaming a Class for Text Formatting

When dealing with instances of one of your classes, the ability to use the insertion and extraction operators << and >> is nice.

To use these operators, you overload them to take parameters of your class. Sounds easy, doesn’t it? When people first find out about overloading the insertion and extraction operators, the process often seems so much harder than it really is.

Here’s the scoop: If you have a class, say MicrowaveOven, and you have an instance of this class, say myoven, all you do to accomplish the overloading of an operator is code a function that takes as a parameter a stream and an object and writes the members of the object to the stream. Then you will be able to code one of the following lines:

cout << myoven;

outfile << myoven;

Now what if you want to code an operator that reads from a stream? Then all you do is write a function that reads the members from a stream if you want to code one of the following lines:

cin >> myoven;

infile >> myoven;

Again, no biggie. The key is what to call the function.

Remember that cout << myoven actually calls a function called <<. Here’s the function header:

ostream &operator <<(ostream &out, MicrowaveOven &oven)

This technique isn’t as hard to remember as you may think. First, always remember that every type in this is a reference. (That makes sense when you look at cout << myoven. The second parameter, myoven, is not a pointer. And you normally don’t want to pass objects around directly, so that leaves only one possibility: passing it by reference.)

Second, remember that the function must return the stream that it’s working with. Returning the stream allows you to chain together operators like this:

cout << “hi” << myoven << 123 << endl;

Finally, remember that the operator function takes two parameters. You can see their order when you look at the order of cout << myoven. The first is the stream, and the second is your class. And thus, when you put this all together, you get the function header we just described, that is

ostream &operator <<(ostream &out, MicrowaveOven &oven)

Now what do you do with this function that you wrote? You just write to the stream passed into it! What do you write? Whatever you want! It’s true: Because you designed the class that the function takes as a parameter, you decide how the output looks when you write the object to a stream. So pretend that this is your MicrowaveOven class:

class MicrowaveOven

{

public:

int HighVoltageRadiation;

int RadioactiveFoodCount;

int LeakLevel;

string OvenName;

};

Then your insertion function may look like this:

ostream &operator <<(ostream &out, MicrowaveOven &oven)

{

out << “High Voltage Radiation: “;

out << oven.HighVoltageRadiation << endl;

out << “Radioactive Food Count: “;

out << oven.RadioactiveFoodCount << endl;

out << “Leak Level: “;

out << oven.LeakLevel << endl;

out << “Oven Name: “;

out << oven.OvenName << endl;

return out;

}

Now for some points about the preceding code:

♦ We took complete liberty on how we wanted the object to look on the stream. For each member variable, we wrote a description, a colon, a space, and then a value. We then put an endl. Would you like the output to look different? Then go for it! It’s your choice how you want the output to look.

♦ We returned the same output stream that came in as the first parameter. This is important!

♦ When we wrote to the stream, we wrote to out, not to cout. If we messed up and wrote to cout, this function would not work properly when used with a file. If we tried myfile << myoven, the information would just go to cout, not into the file. Oops!

In this function, we accessed only the public member variables of the oven instance. As it stands, we can’t access the private members because this function is not a member of MicrowaveOven. (Now of course, MicrowaveOven doesn’t actually have any private members, but most of your classes probably do.) To access the private members, make this function a friend of MicrowaveOven by adding this inside the MicrowaveOven class:

friend ostream &operator <<(ostream &out,

MicrowaveOven &oven);

Here’s a similar function for reading from a stream:

istream &operator >(istream &in, MicrowaveOven &oven)

{

in >> oven.HighVoltageRadiation;

in >> oven.RadioactiveFoodCount;

in >> oven.LeakLevel;

in >> oven.OvenName;

return in;

}

You can see that the format of this function is like that of the insertion operator: The function returns a reference to the stream, and for parameters, the function takes a reference to a stream and a reference to a MicrowaveOven object.

And as before, we had complete freedom on how we wanted to read this in. We chose to just read in each member separately. That means that if we call this function by using cin like this

cin > myoven;

then when we run this line, we could type the member values on one line with spaces, or on separate lines, or any combination:

1234 5555

1054 “Buzz”

And that’s it! Using the insertion and extraction operators isn’t really magical at all.

image To use the insertion and extraction operators, remember that you simply write two functions, one for each operator. These functions write to a stream or read from it. And always remember the two most important aspects of these functions:

♦ Remember to return the stream at the end!

♦ Remember to use references!

Manipulating a Stream

A lot of people see this kind of thing:

cout << “Hello” << endl;

and wonder what on Earth endl is. Is it a variable? Is it a keyword? What is it? And how can you add your own things like it? In this section, we answer this and every other question you ever had about these strange little creatures called manipulators.

What’s a manipulator?

What exactly is endl? Here’s the answer, and it might surprise you: endl is a function. However, you may notice that it has no parentheses. And so, you’re not actually calling the function.

So what are you doing? (Seems as though we’ve made the story even more complicated.) In this section, we show you that a manipulator is actually the address of a function. How’s that for a strange thought? Read on.

To clarify exactly what endl is, think about this:

cout << endl;

And think about the operator function, <<. By writing cout << endl, you are calling an overloaded insertion operator function and passing in two parameters, cout and endl. The first parameter, cout, is an instance of ostream. The second parameter, endl, is the address of a function. Yes, when you type a function name but don’t include parentheses, you are giving the address of the function rather than calling the function.

And so, somewhere out there (in the standard header files, actually) is an overloaded insertion function that takes both an ostream and the address of a function. Now the thing about function addresses is that the type of a function pointer is based on the function’s return type and parameter types. Thus, pointers to these two functions have the same type:

void WriteMe(int x, char c);

void AlwaysAndForever(int y, char x);

Even though the names of the parameters are different, the types of the parameters are the same. That’s why pointers to the two functions have the same type. But pointers to the following two functions do not have the same type:

void SomethingForNothing(int x);

int LeaveMeAlone(int y, int z);

The functions do not have the same type because their prototypes are different. The first takes a single integer as a parameter and returns a void. The second takes two integers as parameters and returns an integer.

Now here’s the prototype for the endl function:

ostream& endl(ostream& outs);

This function takes a reference to ostream and returns a reference to ostream. And here’s a sufficient typedef for a pointer to this function:

typedef ostream& (*omanip)(ostream&);

This defines a new type called omanip, which is a pointer to a function that takes as a parameter a reference to ostream and returns a reference to ostream. Perfect! Therefore, if we have a variable of type omanip, we can set it to the address of the endl function.

So now back to this:

cout << endl;

For this manipulator to work, you need an overloaded insertion operator function that takes two parameters: first a reference to ostream (for the cout) and then omanip. Yes, the second parameter must be a reference to omanip because the second item in cout << endl isomanip.

image If you’re not clear on why endl is omanip, think about this: There’s a function called endl, and to call that function, you would type its name, an opening parenthesis, some parameters, and then a closing parenthesis. But if you leave off the parentheses, you’re just taking the address of the function. And the type omanip, which we defined earlier, is exactly that: an address to a function. But on top of being an address, the endl function’s prototype matches that for the omanip type. Therefore, we can say that endl is of the type omanip. Whew.

Here’s a possibility for the header of the overloaded insertion operator:

ostream& operator<<(ostream& out, omanip func);

You can see the parameters that this function takes: First, it takes a reference to ostream and then omanip.

But remember what we’re doing. We’re trying to explain how this manipulator works:

cout << endl;

Two functions are involved. Here are their headers:

ostream& endl(ostream& outs);

ostream& operator<<(ostream& out, omanip func);

When you type cout << endl, you are not calling the endl function. Instead, you are calling this operator<< function because endl by itself — without parentheses — is nothing more than the address of the endl function. And the address is of type omanip.

Thus, when you type cout << endl, you are calling this operator<< function, passing in cout and endl.

Here’s the operator<< function in its entirety:

ostream& operator<<(ostream &out, omanip func)

{

return (*func)(out);

}

The second parameter is called func. When you call cout << endl, you are passing endl in as the func parameter. And what does this operator<< function do? It calls the function passed into it. Thus, it calls endl. Huh?

Why did we go through all this rigmarole, if, ultimately, we’re just calling this mysterious endl function? Here’s why. Believe it or not, the use of endl is all about aesthetics. The following line is short and clear:

cout << “hello” << endl;

And to make this line work, you need an overloaded insertion operator. Fortunately, the operator<< function we’ve been talking about does the trick.

Finally, the computer eventually calls the endl function itself, and that function does the actual work of adding a newline character to the stream. Wow, all that just for a newline! And you thought you had it rough! Imagine being the compiler!

Now this isn’t the only way to accomplish coding a manipulator, as we explain in the following section, “Writing your own manipulator.” In that section, we use a slightly different approach that works equally well. But the technique we’ve been describing works, too.

image Time for some honesty: We did doctor the overloaded insertion operator function a bit, because, really, this function is a member of ostream. But the overloaded insertion operator function works equally well as a standalone function, as we’ve described in this section.

Writing your own manipulator

You can write your own manipulators in several ways. The goal is to allow for this type of code

cout << mymanipulator;

which causes a function, such as this, to get called:

ostream &operator << (ostream &out, somespecialtype a);

Now think about the overloading that goes on here: Several operator << functions are available; ultimately, they all differ in the type of the second parameter, where we wrote somespecialtype. And whatever mymanipulator is, it must be the somespecialtype type as well. But on top of it, this type must be unique: There cannot already be an overloaded function that has that type! Unique, unique, unique!

Although in the “What’s a manipulator?” section, earlier in this chapter, we give all the gory details on how the endl manipulator works, we think that amount of detail is a bit too complicated. We’d rather use a slightly different approach for our own manipulators. Here’s what we’re going to do in the following example: We want to make sure that we have a unique type and that the manipulator is an object of that type. As with other manipulators, function pointers work well. But for the function pointer to be unique, its return type and parameter types must be unique. That’s not too hard; to guarantee that no other function has that prototype, we’re going to make our own special type — a structure — and use that as the parameter for the function, like this:

struct FullOvenManip {};

void FullOvenInfo(FullOvenManip x) {}

Check this sample carefully. We created a structure called FullOvenManip. This structure has nothing in it; its sole purpose in life is to provide for a unique parameter experience. Yee-hah! And the function FullOvenInfo takes this structure as a parameter. Considering that we just invented this structure, we can be quite certain that no other function in the C++ header files matches this prototype. More than certain, in fact. We’d be willing to bet our Hollywood Hills mansion (which we don’t have, so we have nothing to lose!).

Now we can provide an overloaded operator >> function. That function takes a pointer to the FullOvenInfo function. But to do that, we had better typedef:

typedef void(*FullPtr)(FullOvenManip);

This line of code creates a type called FullPtr, which is a pointer to a function that takes a FullOvenManip parameter and returns a void. We can think of only one function that does that! It’s the FullOvenInfo function. Woo-Hoo!

imageWhen writing your own manipulators, don’t shy away from using typedef. The manipulator concept is confusing and can be a serious struggle for many of us to keep straight. By using a typedef, you can simplify your life a bit.

Here’s the overloaded operator >> function header:

ostream &operator << (ostream &out, FullPtr);

You can see the second parameter: It’s a FullPtr. And look at this code:

cout << FullOvenInfo;

The FullOvenInfo item is also a FullPtr because it’s a pointer to a function that takes a FullOvenManip. Voilà. That does the trick.

Listing 5-1 is a really great example of all this!

Listing 5-1: Using Manipulators

#include <iostream>

#include <string>

#include <fstream>

#include <map>

using namespace std;

class MicrowaveOven

{

friend ostream &operator <<(ostream &out,

MicrowaveOven &oven);

public:

typedef map<ostream *, bool> FlagMap;

int HighVoltageRadiation;

int RadioactiveFoodCount;

int LeakLevel;

string OvenName;

static FlagMap Flags;

};

MicrowaveOven::FlagMap MicrowaveOven::Flags;

ostream &operator <<(ostream &out, MicrowaveOven &oven)

{

bool full = true;

MicrowaveOven::FlagMap::iterator iter =

MicrowaveOven::Flags.find(&out);

if (iter != MicrowaveOven::Flags.end())

{

full = iter->second;

}

if (full)

{

out << “High Voltage Radiation: “;

out << oven.HighVoltageRadiation << endl;

out << “Radioactive Food Count: “;

out << oven.RadioactiveFoodCount << endl;

out << “Leak Level: “;

out << oven.LeakLevel << endl;

out << “Oven Name: “;

out << oven.OvenName;

}

else

{

out << oven.HighVoltageRadiation << “,”;

out << oven.RadioactiveFoodCount << “,”;

out << oven.LeakLevel << “,”;

out << oven.OvenName;

}

return out;

}

istream &operator >(istream &in, MicrowaveOven &oven)

{

in >> oven.HighVoltageRadiation;

in >> oven.RadioactiveFoodCount;

in >> oven.LeakLevel;

in >> oven.OvenName;

return in;

}

struct FullOvenManip {};

void FullOvenInfo(FullOvenManip x) {}

typedef void(*FullPtr)(FullOvenManip);

ostream &operator << (ostream &out, FullPtr)

{

MicrowaveOven::Flags[&out] = true;

return out;

}

struct MinOvenManip {};

void MinOvenInfo(MinOvenManip x) {}

typedef void(*MinPtr)(MinOvenManip);

ostream &operator << (ostream &out, MinPtr)

{

MicrowaveOven::Flags[&out] = false;

return out;

}

int main()

{

MicrowaveOven myoven;

myoven.HighVoltageRadiation = 9832;

myoven.RadioactiveFoodCount = 7624;

myoven.LeakLevel = 3793;

myoven.OvenName = “Burnmaster”;

cout << myoven << endl;

cout << “============” << endl;

cout << FullOvenInfo << myoven << endl;

cout << “============” << endl;

cout << MinOvenInfo << myoven << endl;

return 0;

}

The code in Listing 5-1 creates two manipulators, one called FullOvenInfo and one called MinOvenInfo. When you use one of these manipulators, as in the following line, you call our overloaded operator >> function:

cout << FullOvenInfo << myoven << endl;

That function works with a map to keep track of which stream you are manipulating. The map lives as a static member in the MicrowaveOven class. So when you use the FullOvenInfo manipulator on cout, the map’s item for cout gets a true. And when you use theMinOvenInfo manipulator on cout, the map’s item for cout gets a false.

So why did we bother with the map? The idea is that you may be working with multiple streams, such as one for an ofstream file and one for cout, and you may want some to show the full information via the FullOvenInfo manipulator and some to show the minimal information via the MinOvenInfo. And so we keep a map based on the stream. And the really great thing is that the code actually works. In the overloaded operator >> function that prints a MicrowaveOven object, you can see how we check the map for a true or false for the current stream.

When you run this program, you see this output:

High Voltage Radiation: 9832

Radioactive Food Count: 7624

Leak Level: 3793

Oven Name: Burnmaster

============

High Voltage Radiation: 9832

Radioactive Food Count: 7624

Leak Level: 3793

Oven Name: Burnmaster

============

9832,7624,3793,Burnmaster

We printed the same object three times. The first one demonstrates the default: If you provide no manipulators, you get a full listing. We handled that in the overloaded operator >> for printing a MicrowaveOven object:

bool full = true;

MicrowaveOven::FlagMap::iterator iter =

MicrowaveOven::Flags.find(&out);

if (iter != MicrowaveOven::Flags.end())

{

full = iter->second;

}

Remember that iterator is really a pointer to the map entry. And so we call find to determine if the item is inside the map entry. If it’s not, find returns Flags.end(). (That’s just the way the find function works. If we’d written the map class, we would have done things differently. Can you say simplify, simplify, simplify rather than obfuscate, obfuscate, obfuscate?)

And if we don’t get back Flags.end(), that means we found the item in the map. So in that case, we use iter->second to obtain the value.

But notice what happens if we do get back Flags.end(), meaning the stream wasn’t found in the map. Then we just stick with the default value for full, which was true:

bool full = true;

And so you can see that these output lines will function properly:

cout << myoven << endl;

cout << “============” << endl;

cout << FullOvenInfo << myoven << endl;

cout << “============” << endl;

cout << MinOvenInfo << myoven << endl;

The first line with myoven line uses the default, which is a full listing. The second line with myoven says to definitely give us a full listing, using the FullOvenInfo manipulator. And the third line with myoven gives a minimal listing, which we chose with the MinOvenInfomanipulator.

Life is good.