|
|
|
|
Templatized pointer Stash
Reorganizing the PStash code into
a template isn’t quite so simple because there are a number of member
functions that should not be
inlined. However, as a template
those function definitions still belong in the
header file (the compiler and
linker take care of any multiple definition problems). The code looks quite
similar to the ordinary PStash except that you’ll notice the size
of the increment (used by inflate( )) has been templatized as a
non-class parameter with a default value, so that the increment size can be
modified at the point of instantiation (notice that this means that the
increment size is fixed; you may also argue that the increment size should be
changeable throughout the lifetime of the object):
//: C16:TPStash.h
#ifndef TPSTASH_H
#define TPSTASH_H
template<class T, int incr = 10>
class PStash {
int quantity; // Number of storage spaces
int next; // Next empty space
T** storage;
void inflate(int increase = incr);
public:
PStash() : quantity(0), next(0), storage(0) {}
~PStash();
int add(T* element);
T* operator[](int index) const; // Fetch
// Remove the reference from this PStash:
T* remove(int index);
// Number of elements in Stash:
int count() const { return next; }
};
template<class T, int incr>
int PStash<T, incr>::add(T* element) {
if(next >= quantity)
inflate(incr);
storage[next++] = element;
return(next - 1); // Index number
}
// Ownership of remaining pointers:
template<class T, int incr>
PStash<T, incr>::~PStash() {
for(int i = 0; i < next; i++) {
delete storage[i]; // Null pointers OK
storage[i] = 0; // Just to be safe
}
delete []storage;
}
template<class T, int incr>
T* PStash<T, incr>::operator[](int index) const {
require(index >= 0,
"PStash::operator[] index negative");
if(index >= next)
return 0; // To indicate the end
require(storage[index] != 0,
"PStash::operator[] returned null pointer");
// Produce pointer to desired element:
return storage[index];
}
template<class T, int incr>
T* PStash<T, incr>::remove(int index) {
// operator[] performs validity checks:
T* v = operator[](index);
// "Remove" the pointer:
if(v != 0) storage[index] = 0;
return v;
}
template<class T, int incr>
void PStash<T, incr>::inflate(int increase) {
const int psz = sizeof(T*);
T** st = new T*[quantity + increase];
memset(st, 0, (quantity + increase) * psz);
memcpy(st, storage, quantity * psz);
quantity += increase;
delete []storage; // Old storage
storage = st; // Point to new memory
}
#endif // TPSTASH_H ///:~
The default increment size used here is
small to guarantee that calls to inflate( ) occur. This way we can
make sure it works correctly.
To test the
ownership control of the templatized PStash, the
following class will report
creations and destructions of
itself, and also guarantee that all objects that have been created were also
destroyed. AutoCounter will allow only objects of its type to be created
on the
stack:
//: C16:AutoCounter.h
#ifndef AUTOCOUNTER_H
#define AUTOCOUNTER_H
#include "../require.h"
#include <iostream>
#include <set> // Standard C++ Library container
#include <string>
class AutoCounter {
static int count;
int id;
class CleanupCheck {
std::set<AutoCounter*> trace;
public:
void add(AutoCounter* ap) {
trace.insert(ap);
}
void remove(AutoCounter* ap) {
require(trace.erase(ap) == 1,
"Attempt to delete AutoCounter twice");
}
~CleanupCheck() {
std::cout << "~CleanupCheck()"<< std::endl;
require(trace.size() == 0,
"All AutoCounter objects not cleaned up");
}
};
static CleanupCheck verifier;
AutoCounter() : id(count++) {
verifier.add(this); // Register itself
std::cout << "created[" << id << "]"
<< std::endl;
}
// Prevent assignment and copy-construction:
AutoCounter(const AutoCounter&);
void operator=(const AutoCounter&);
public:
// You can only create objects with this:
static AutoCounter* create() {
return new AutoCounter();
}
~AutoCounter() {
std::cout << "destroying[" << id
<< "]" << std::endl;
verifier.remove(this);
}
// Print both objects and pointers:
friend std::ostream& operator<<(
std::ostream& os, const AutoCounter& ac){
return os << "AutoCounter " << ac.id;
}
friend std::ostream& operator<<(
std::ostream& os, const AutoCounter* ac){
return os << "AutoCounter " << ac->id;
}
};
#endif // AUTOCOUNTER_H ///:~
The AutoCounter class does two
things. First, it sequentially numbers each instance of AutoCounter: the
value of this number is kept in id, and the number is generated using the
static data member count.
Second, and more complex, a
static instance (called verifier) of
the nested class CleanupCheck keeps track of all of the
AutoCounter objects that are created and destroyed, and reports back to
you if you don’t clean all of them up (i.e. if there is a memory leak).
This behavior is accomplished using a
set
class from the Standard C++ Library, which is a wonderful example of how
well-designed templates can make life easy (you can learn about all the
containers in the Standard C++ Library in Volume 2 of this book, available
online).
The set class is templatized on
the type that it holds; here it is instantiated to hold AutoCounter
pointers. A set will allow only one instance of each distinct object to
be added; in add( ) you can see this take place with the
set::insert( ) function. insert( ) actually informs you
with its return value if you’re trying to add something that’s
already been added; however, since object addresses are being added we can rely
on C++’s guarantee that all objects have unique
addresses.
In remove( ),
set::erase( ) is used to remove an AutoCounter pointer from
the set. The return value tells you how many instances of the element
were removed; in our case we only expect zero or one. If the value is zero,
however, it means this object was already deleted from the set and
you’re trying to delete it a second time, which is a programming error
that will be reported through
require( ).
The destructor for CleanupCheck
does a final check by making sure that the size of the set is zero
– this means that all of the objects have been properly cleaned up. If
it’s not zero, you have a memory leak, which is reported through
require( ).
The constructor and destructor for
AutoCounter register and unregister themselves with the verifier
object. Notice that the constructor, copy-constructor, and assignment operator
are private, so the only way for you to create an object is with the
static create( ) member function – this is a simple example of
a factory, and it
guarantees that all objects are created on the heap, so
verifier will not get confused over assignments and
copy-constructions.
Since all of the member functions have
been inlined, the only reason for the implementation file is to contain the
static data member definitions:
//: C16:AutoCounter.cpp {O}
// Definition of static class members
#include "AutoCounter.h"
AutoCounter::CleanupCheck AutoCounter::verifier;
int AutoCounter::count = 0;
///:~
With AutoCounter in hand, we can
now test the facilities of the PStash. The following example not only
shows that the PStash destructor cleans up all the objects that it
currently owns, but it also demonstrates how the AutoCounter class
detects objects that haven’t been cleaned up:
//: C16:TPStashTest.cpp
//{L} AutoCounter
#include "AutoCounter.h"
#include "TPStash.h"
#include <iostream>
#include <fstream>
using namespace std;
int main() {
PStash<AutoCounter> acStash;
for(int i = 0; i < 10; i++)
acStash.add(AutoCounter::create());
cout << "Removing 5 manually:" << endl;
for(int j = 0; j < 5; j++)
delete acStash.remove(j);
cout << "Remove two without deleting them:"
<< endl;
// ... to generate the cleanup error message.
cout << acStash.remove(5) << endl;
cout << acStash.remove(6) << endl;
cout << "The destructor cleans up the rest:"
<< endl;
// Repeat the test from earlier chapters:
ifstream in("TPStashTest.cpp");
assure(in, "TPStashTest.cpp");
PStash<string> stringStash;
string line;
while(getline(in, line))
stringStash.add(new string(line));
// Print out the strings:
for(int u = 0; stringStash[u]; u++)
cout << "stringStash[" << u << "] = "
<< *stringStash[u] << endl;
} ///:~
When AutoCounter elements 5 and 6
are removed from the PStash, they become the responsibility of the
caller, but since the caller never cleans them up they cause memory leaks, which
are then detected by AutoCounter at run time.
When you run the program, you’ll
see that the error message isn’t as specific as it could be. If you use
the scheme presented in AutoCounter to discover memory leaks in your own
system, you will probably want to have it print out more detailed information
about the objects that haven’t been cleaned up. Volume 2 of this book
shows more sophisticated ways to do
this.
|
|
|