So you started learning to code in C++. You know how to create a class, learned the golden rule that a “deconstructor” will always exist, and have decided that it’s time to learn about streams. What a great tool to use if you know what it is all about.

I assume you know how to use streams for writing some data to a file and not only to the console. If this statement is false in your case, I advise you to read some of my other articles on this topic, published here on the Developer Shed Network. Once you understand that, then handling the serialization of objects with just a few lines will become manageable.

However, there is a problem. When you try to pass to the stream, your very own and custom class (with its own internal members) the compiler just puts out an error that this is an invalid move. The reason for this issue is that the streams are compatible by default only with the default types known by C++. If all this is true in your case, you are in the right place, as here I will show you how to resolve it.

This process, known also as insertion and extraction in streams, is an easy and practical way to resolve serialization. In the world of C++, I/O tasks are completed through the iostream library, which is an object-oriented class library that makes use of multiple and virtual inheritance. This is provided inside the “iostream” library as a component of C++.

Why Should You Make Your Class Compatible with Streams?

Making your class compatible with the streaming function is in reality a straightforward task. You simply output anything that you need inside an overload of the insertion/extraction operator. Let me present  the advantage of implementing a streaming process first.

I have constructed a simple class that represents a car inside a database. As with any car at this level, it will have a unique ID and a name:

class car

{

public:

car(){};

car(string carType, int ID)

{m_carType = carType; m_ID = ID;}

virtual ~car() {};

string getName(){return m_carType;}

int getID(){return m_ID;}

void setCar(string carN)

{ m_carType = carN;}

private:

string m_carType;

int m_ID;

};

How do we serialize this object easily? We might just use the Iostream library of C++. Now I will present my examples in the cout (the console stream), but this can be generalized to any kind of stream.

Michael, for instance, assumes that he as a programmer is more interested in the ID, so apply a print like this:

car A(1, “Ford”);

cout << A.getID() << ‘t’ << A.getName();

Later, Joe enters the picture, and he believes that, for the user interface, the name is more important. So he decides to push the ID into the background the ID, and inverts the printing order:

car B(1, “Ford”);

cout << B.getName()<< ‘t’ <<B.getID();

Now imagine doing this inside a serialization program. Each time Joe tries to read in the data written by Michael (or vice versa), most probably undefined behavior will occur, because an inconsistency of the two serializations exists. Converting the name to a number and the number to a name is not a good idea at all.

Now all of this, placed inside of a single main function, may look trivial, but imagine this is inside a large application — one that has tens of thousands of lines. Things will be much harder to synchronize once a group of programmers start working on this program, not just you.

Besides, explicitly writing down every time how the members of the class should be printed is a time consuming activity — one that will extend the length of the code you need to spend looking over the code, and increase the number of places where errors can occur. We should be able to directly apply the insertion/extraction operator to our class, and write all this just once in an obvious place for maintenance purposes.

The iostream members will accept any basic build types like char*, int, string, and so forth. All we have to do is make our class a basic build type as well, and we will accomplish this by defining the operator overload for it. Proceed to the next page to see how to do so.

The Insertion

The insertion operation is completed by the >> operator. It is a smart move from the designing team to overload the bitwise shift operator for types where you cannot do this, indeed where this does not make any sense. Reusability is once again scheduled.

The first task we need to do is define the function of the operator overload like this:

ostream& operator<< (ostream& out, const car& per);

This is overloading the operator << (in fact it defines the operator) for types where, on the left side of the operator is an ostream, and on the right side a car type. The const assures the input class that through this process the data of the “per” will not change, and the reference passing of the argument is for performance purposes, as we avoid creating a new object and copying the data from the input inside it.

Now all we need to do is perform the insertion of the class inside the function into the ostream. Nevertheless, here we have a small design issue. We need to access all of the data in the “car” class inside the function, as we might or might not have functions that guarantee access to its private or protected members.

Here for instance we have the getName() and getID() public methods for this, but designing a class that will not divulge a member that we still want to serialize is surely an achievable task. Besides calling, a function can decrease the performance of your program. We need to make sure that we have access to all of the members of the class, so the road we need to pass on is the friend keyword.

friend ostream& operator<< (ostream& out, const car& per);

Applying the upper declaration of the function inside the car class will point out to the compiler that this function is a friend with the class and guarantee access to all non-public members/functions, just as if those were public. Now we can define the function outside the class in the following manner:

ostream& operator<< (ostream& out, const car& per)

{

out << per.m_carType << ‘t’;

out << per.m_ID << endl;

return out;

}

Inside the function, we just pass each build in type C into the stream, and you might want to make sure that the members are separated by some white spaces (or else you will see a long sequence of chars in your streams). Also, make sure to call a flush at the end of your streaming (here the endl has encapsulated that option); otherwise, they might just be pushed into the buffer and, in the end, not displayed properly.

The Extraction

The insertion assumes that we take the same passes, with just a couple of differences. The direction of the operator changes; now you need to overload the operator >>. You need to declare/define the function for an ifstream type.

As long as you know how the input will look, your task is trivial. For example, for the upper class, I know that the order is name followed by the ID number, and all this is separated by white spaces (the newline and the tab). So I just read it in, maintaining the order of the output.

So declare the friend function inside the class:

friend istream& operator>> (istream& out, car& per);

Define the function as follows:

istream& operator>> (istream& in, car& per)

{

in >> per.m_carType;

in >> per.m_ID;

return in;

}

Here let’s point out that the input argument of the class is no longer const, as we will modify its members with the input data. Returning the stream (here and at the insertion process) is required so that we can maintain the chain affect of the insertion/extraction process like this:

cin << “Yeti” << endl ;

Now that we have this entire task completed, let us see it in action:

// first construct a few items from the upper

car A(“Ford”, 1);

car B(“Toyota”, 2);

// create a ofstream into what will print it

ofstream output(“OutIn.txt”);

 

output << A <<B; // this will print all to the first person

output.close();

// create a ifstream for now to read in

car C,D;

ifstream input(“OutIn.txt”);

input >> C >> D;

 

cout << C <<D << endl;

input.close();

The output result is exactly what we were looking for:

Ford 1

Toyota 2

Press any key to continue . . .

Following the convention within the stream and taking the steps outlined above, instead of writing a function like writeToStream, will make your code easier to read. Now we have a centerpiece that we can easily maintain and make sure there is no conflict is between the input and the output.

Our type is now included between the basic build types, so if we want another class that encapsulates our car class inside its insertion and extraction function, we don’t need to serialize our object anymore; just call the operator on it, as we’ve already done here.

However, if you want to serialize your own data throughout your files in a system that has a more complicated build, I strongly recommend that you take a look at Boost’s serialization classes. They offer you this support in a simple, efficient, and portable way.

LEAVE A REPLY

Please enter your comment!
Please enter your name here