You can change the type of container that this program uses with two lines. Instead of including <vector>, you include <list>, and in the first typedef you say:.
typedef std::list<Shape*> Container;
instead of using a vector. Everything else goes untouched. This is possible not because of an interface enforced by inheritance (there is little inheritance in the STL, which may come as a surprise), but because the interface is enforced by a convention adopted by the designers of the STL, precisely so you could perform this kind of interchange. Now you can easily switch between vector and list or any other container that supports the same interface and see which one works fastest for your needs.
Containers of strings
In the previous example, at the end of main( ) it was necessary to move through the whole list and delete all the Shape pointers:.
for(Iter j = shapes.begin();
j != shapes.end(); j++)
delete *j;
This highlights what could be seen as an oversight in the STL: there’s no facility in any of the STL containers to automatically delete the pointers they contain, so you must do it by hand. It’s as if the assumption of the STL designers was that containers of pointers weren’t an interesting problem.
Automatically deleting a pointer turns out to be a rather aggressive thing to do because of the multiple membership problem. If a container holds a pointer to an object, it’s not unlikely that pointer could also be in another container. A pointer to an Aluminum object in a list of Trash pointers could also reside in a list of Aluminum pointers. If that happens, which list is responsible for cleaning up that object—that is, which list "owns" the object?.
This question is virtually eliminated if the object rather than a pointer resides in the list. Then it seems clear that when the list is destroyed, the objects it contains must also be destroyed. Here, the STL shines, as you can see when creating a container of string objects. The following example stores each incoming line as a string in a vector<string>:
//: C07:StringVector.cpp
// A vector of strings
#include <fstream>
#include <iostream>
#include <iterator>
#include <sstream>
#include <string>
#include <vector>
#include "../require.h"
using namespace std;
int main(int argc, char* argv[]) {
char* fname = "StringVector.cpp";
if(argc > 1) fname = argv[1];
ifstream in(fname);
assure(in, fname);
vector<string> strings;
string line;
while(getline(in, line))
strings.push_back(line);
// Do something to the strings...
int i = 1;
vector<string>::iterator w;
for(w = strings.begin();
w != strings.end(); w++) {
ostringstream ss;
ss << i++;
*w = ss.str() + ": " + *w;
}
// Now send them out:
copy(strings.begin(), strings.end(),
ostream_iterator<string>(cout, "\n"));
// Since they aren't pointers, string
// objects clean themselves up!
} ///:~
Once the vector<string> called strings is created, each line in the file is read into a string and put in the vector:.
while(getline(in, line))
strings.push_back(line);
The operation that’s being performed on this file is to add line numbers. A stringstream provides easy conversion from an int to a string of characters representing that int.
Assembling string objects is quite easy, since operator+ is overloaded. Sensibly enough, the iterator w can be dereferenced to produce a string that can be used as both an rvalue and an lvalue:.
*w = ss.str() + ": " + *w;
You may be surprised that you can assign back into the container via the iterator , but it’s a tribute to the careful design of the STL.
Because the vector<string> contains the objects themselves, a number of interesting things take place. First, no explicit cleanup of the string objects on your part is necessary. Even if you were to put addresses of the string objects as pointers into other containers, it’s clear that strings is the "master list" and maintains ownership of the objects.
Second, you are effectively using dynamic object creation, and yet you never use new or delete! That’s because, somehow, it’s all taken care of for you by the vector because it stores copies of the objects you give it. Thus your coding is significantly cleaned up.
Inheriting from STL containers
The power of instantly creating a sequence of elements is amazing, and it makes you realize how much time you may have spent (or rather wasted) in the past solving this particular problem. For example, many utility programs involve reading a file into memory, modifying the file, and writing it back out to disk. You might as well take the functionality in StringVector.cpp and package it into a class for later reuse.
Now the question is: do you create a member object of type vector, or do you inherit? A general object-oriented design guideline is to prefer composition (member objects) over inheritance, but the standard algorithms expect sequences that implement a specified interface, so inheritance is often called for.
//: C07:FileEditor.h
// File editor tool
#ifndef FILEEDITOR_H
#define FILEEDITOR_H
#include <iostream>
#include <string>
#include <vector>
class FileEditor :
public std::vector<std::string> {
public:
void open(const char* filename);
FileEditor(const char* filename) {
open(filename);
}
FileEditor() {};
void write(std::ostream& out = std::cout);
};
#endif // FILEEDITOR_H ///:~
Note the careful avoidance of a global using namespace std statement here, to prevent the opening of the std namespace in every file that includes this header.
The constructor opens the file and reads it into the FileEditor, and write( ) puts the vector of string onto any ostream. Notice in write( ) that you can have a default argument for the reference.
The implementation is quite simple:.
//: C07:FileEditor.cpp {O}