Serializing Containers
In our sample, a Drawing object can contain multiple Polygon objects (which I will store in a vector, one of the container templates in the Standard Library), and each Polygon object can contain multiple Vertex objects (again, which I'll store in a vector).
The serialization library includes features for saving arrays and containers. Because you can store pointers into arrays, the serialization library also supports pointers. Think about this: If you have an array containing Vertex pointers and you write that array to a file directly (as-is), you'll have a bunch of pointers stored in the file, not the actual Vertex data. Those pointers are just numbers (memory locations) that would be meaningless when read back in later. Thus, the library is smart enough to grab the actual data for the object rather than the pointers.
To allow for storing the objects that are containers, you make each of your classes serializable. In the serialization methods, you read and write the containers just as you would any other member. Your containers can be simple built-in arrays (such as Vertex *vertices[10];), or they can be containers from the Standard Library. Because this is the 21st century, I like to keep up with the times, and so I chose in this example to use the Standard Library.
Although you could write code in your serialize class that climbs through the container and writes out each member, you don't have to. Instead, the library is smart enough to traverse through the container itself. All you need to do is write out the container, like this, where vertices is a container:
ar & vertices;
And the library does the rest. Don't believe me? Here's the code for the Polygon class, again with the serialization portions in bold:
class Polygon { private: vector<Vertex *> vertices; friend class boost::serialization::access; template<class Archive> void serialize(Archive & ar, const unsigned int version) { ar & vertices; } public: void addVertex(Vertex *v) { vertices.push_back(v); } void dump() { for_each(vertices.begin(), vertices.end(), mem_fun(&Vertex::dump)); } };
First, notice that I'm using a vector to store the vertices. (If you're new to templates, just think of vector<Vertex *> as meaning a vector that stores pointers to Vertex instances because that is, in fact, all it is.) Next, in the serialize function I don't climb through the vector, writing each member. Instead, I just read and write the entire vector:
ar & vertices;
The two public methods are handy for manipulating the polygon. The first, addVertex, lets you add another vertex to the polygon; it uses the push_back method, which is the standard method for adding an item to a vector. The dump function climbs through the vector, writing out each vector to the standard output. Even to some of you seasoned seeplusplusers, this line might look unfamiliar:
for_each(vertices.begin(), vertices.end(), mem_fun(&Vertex::dump));
This is a little trick. It's not part of the serialization library; it is part of the Standard Library and available to your C++ programs today at no extra charge and with a money-back guarantee. The word for_each is actually a function, and it takes three parameters: the starting position in the container, the ending condition, and a function to call for each item in the container. (Depending on your implementation of C++, you might have to #include <algorithm> to get for_each. I used the GNU library, and #include <vector> picked it up for me.) In the case of my example, the function I for the third parameter of for_each is the dump member function of the Vertex class. But there's a problem: You can't just call a member function by itself; you need to call it for a particular object. That's where the mem_fun member function adaptor comes in. It's a special function (also part of the Standard Library) that in this case works together with the for_each function, calling the particular object's dump function. In other words, it binds dump() to the Vertex object that for_each is currently manipulating.
To make a long story short, this for_each call iterates through each Vertex in the entire list, calling Vertex::dump—all in one line of code!
Next, the Drawing class is actually quite similar to the Polygon class, except that instead of containing a container of Vertex objects, it contains Polygon objects. No big deal.
Here, then, is the entire program, including some additional destructors that take care of cleaning up the memory:
#include <iostream> #include <vector> #include <functional> #include <algorithm> #include <fstream> #include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.hpp> #include <boost/serialization/vector.hpp> using namespace std; class Vertex { private: // Code for serialization friend class boost::serialization::access; template<class Archive> void serialize(Archive & ar, unsigned int version) { ar & x; ar & y; } // End code for serialization double x; double y; public: Vertex() {} // Serialization requires a default ctor! ~Vertex() {} Vertex(double newX, double newY) : x(newX), y(newY) {} double getX() const { return x; } double getY() const { return y; } void dump() { cout << x << " " << y << endl; } }; void delete_vertex(Vertex *v) { delete v; } class Polygon { private: vector<Vertex *> vertices; friend class boost::serialization::access; template<class Archive> void serialize(Archive & ar, const unsigned int version) { ar & vertices; } public: ~Polygon() { for_each(vertices.begin(), vertices.end(), delete_vertex); } void addVertex(Vertex *v) { vertices.push_back(v); } void dump() { for_each(vertices.begin(), vertices.end(), mem_fun(&Vertex::dump)); } }; void delete_poly(Polygon *p) { delete p; } class Drawing { private: vector<Polygon *> polygons; friend class boost::serialization::access; template<class Archive> void serialize(Archive & ar, const unsigned int version) { ar & polygons; } public: ~Drawing() { for_each(polygons.begin(), polygons.end(), delete_poly); } void addPolygon(Polygon *p) { polygons.push_back(p); } void dump() { for_each(polygons.begin(), polygons.end(), mem_fun(&Polygon::dump)); } }; string getFileOpen() { // In real life, this would // call a FileOpen dialog of sorts return "c:/myfile.grp"; } string getFileSaveAs() { // In real life, this would // call a FileSave dialog of sorts return "c:/myfile.grp"; } void saveDocument(Drawing *doc, const string &filename) { ofstream ofs(filename.c_str()); boost::archive::text_oarchive oa(ofs); oa << *doc; ofs.close(); } Drawing *openDocument(const string &filename) { Drawing *doc = new Drawing(); std::ifstream ifs(filename.c_str(), std::ios::binary); boost::archive::text_iarchive ia(ifs); ia >> *doc; ifs.close(); return doc; } int main() { /* Polygon *poly1 = new Polygon(); poly1->addVertex(new Vertex(0.1,0.2)); poly1->addVertex(new Vertex(1.5,1.5)); poly1->addVertex(new Vertex(0.5,2.9)); Polygon *poly2 = new Polygon(); poly2->addVertex(new Vertex(0,0)); poly2->addVertex(new Vertex(0,1.5)); poly2->addVertex(new Vertex(1.5,1.5)); poly2->addVertex(new Vertex(1.5,0)); Drawing *draw = new Drawing(); draw->addPolygon(poly1); draw->addPolygon(poly2); // Demonstrate saving a document saveDocument(draw, getFileSaveAs()); // Demonstrate opening a document string filename2 = getFileOpen(); Drawing *doc2 = openDocument(getFileOpen()); doc2->dump(); return 0;*/ Polygon *poly1 = new Polygon(); poly1->addVertex(new Vertex(0.1,0.2)); poly1->addVertex(new Vertex(1.5,1.5)); poly1->addVertex(new Vertex(0.5,2.9)); Polygon *poly2 = new Polygon(); poly2->addVertex(new Vertex(0,0)); poly2->addVertex(new Vertex(0,1.5)); poly2->addVertex(new Vertex(1.5,1.5)); poly2->addVertex(new Vertex(1.5,0)); Drawing *draw = new Drawing(); draw->addPolygon(poly1); draw->addPolygon(poly2); // Demonstrate saving a document saveDocument(draw, getFileSaveAs()); // Demonstrate opening a document string filename2 = getFileOpen(); Drawing *doc2 = openDocument(getFileOpen()); doc2->dump(); delete draw; return 0; }
Now remember the idea here: I'm trying to steer away from the idea that I'm writing Drawing objects to a file. Instead, conceptually I'm considering the Drawing object my document, and I'm storing document files and reading them back in. Those document files have a special file format that I have created specifically for my program, and I assigned them a unique filename extension, .grp, which stands for graphics.
As such, I also created a couple of filler functions: getFileSaveAs and getFileOpen. In this sample, these functions just return a string with a filename hardcoded. In a real program, these functions would get called from a File, Save As and File, Open menu selection, respectively; and would call the system File Open and File Save dialog boxes. These dialog boxes would return a string containing the filename that the user wishes to use. Thus, the user's perspective is the same as ours: opening and saving documents rather than reading and writing Drawing object data to the documents. See the difference in concept, even if they are identical in function?