MindView Inc.
[ Viewing Hints ] [ Revision History ] [ Free Newsletter ]
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ]

Thinking in C++, 2nd ed., Volume 2, Revision 2

©2000 by Bruce Eckel

[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]

9: Building stable systems


Shared objects & reference counting

Reference-counted class hierarchies

Finding memory leaks

  1. For array bounds checking, use the Array template in C16:Array3.cpp of Volume 1 for all arrays. You can turn off the checking and increase efficiency when you’re ready to ship. (This doesn’t deal with the case of taking a pointer to an array, though – perhaps that could be templatized somehow as well).
  2. Use the C10:MemCheck (wrong chapter number) to guarantee that dynamic memory is released properly.
  3. Check for non-virtual destructors in base classes.

The canonical object & singly-rooted hierarchies

An extended canonical form

Design by contract

Integrated unit testing

Dynamic aggregation

[[ This may actually be the “builder” design pattern in some form ]]

The examples we’ve seen so far are illustrative, but fairly simple. It’s useful to see an example that has more complexity so you can see that the STL will work in all situations.

[[ Add a factory method that takes a vector of string]]

The class that will be created as the example will be reasonably complex: it’s a bicycle which can have a choice of parts. In addition, you can change the parts during the lifetime of a Bicycle object; this includes the ability to add new parts or to upgrade from standard-quality parts to “fancy” parts. The BicyclePart class is a base class with many different types, and the Bicycle class contains a vector<BicyclePart*> to hold the various combination of parts that may be attached to a Bicycle:

//: C09:Bicycle.h
// Complex class involving dynamic aggregation
#ifndef BICYCLE_H
#define BICYCLE_H
#include <vector>
#include <string>
#include <iostream>
#include <typeinfo>

class LeakChecker {
  int count;
public:
  LeakChecker() : count(0) {}
  void print() {
    std::cout << count << std::endl; 
  }
  ~LeakChecker() { print(); }
  void operator++(int) { count++; }
  void operator--(int) { count--; }
};

class BicyclePart {
  static LeakChecker lc;
public:
  BicyclePart() { lc++; }
  virtual BicyclePart* clone() = 0;
  virtual ~BicyclePart() { lc--; }
  friend std::ostream& 
  operator<<(std::ostream& os, BicyclePart* bp) {
    return os << typeid(*bp).name();
  }
  friend class Bicycle;
};

enum BPart {
  Frame, Wheel, Seat, HandleBar, 
  Sprocket, Deraileur,
};

template<BPart id> 
class Part : public BicyclePart {
public:
  BicyclePart* clone() { return new Part<id>; }
};

class Bicycle {
public:
  typedef std::vector<BicyclePart*> VBP;
  Bicycle();
  Bicycle(const Bicycle& old);
  Bicycle& operator=(const Bicycle& old);
  // [Other operators as needed go here:]
  // [...]
  // [...]
  ~Bicycle() { purge(); }
  // So you can change parts on a bike (but be 
  // careful: you must clean up any objects you
  // remove from the bicycle!)
  VBP& bikeParts() { return parts; }
  friend std::ostream& 
  operator<<(std::ostream& os, Bicycle* b);
  static void print(std::vector<Bicycle*>& vb, 
    std::ostream& os = std::cout);
private:
  static int counter;
  int id;
  VBP parts;
  void purge();
};

// Both the Bicycle and the generator should 
// provide more variety than this. But this gives
// you the idea.
struct BicycleGenerator {
  Bicycle* operator()() {
    return new Bicycle;
  }
}; 
#endif // BICYCLE_H ///:~

The operator<< for ostream and Bicycle moves through and calls the operator<< for each BicyclePart, and that prints out the class name of the part so you can see what a Bicycle contains. The BicyclePart::clone( ) member function is necessary in the copy-constructor of Bicycle, since it just has a vector<BicyclePart*> and wouldn’t otherwise know how to copy the BicycleParts correctly. The cloning process, of course, will be more involved when there are data members in a BicyclePart.

BicyclePart::partcount is used to keep track of the number of parts created and destroyed (so you can detect memory leaks). It is incremented every time a new BicyclePart is created and decremented when one is destroyed; also, when partcount goes to zero this is reported and if it goes below zero there will be an assert( ) failure.

If you want to change BicycleParts on a Bicycle, you just call Bicycle::bikeParts( ) to get the vector<BicyclePart*> which you can then modify. But whenever you remove a part from a Bicycle, you must call delete for that pointer, otherwise it won’t get cleaned up.

Here’s the implementation:

//: C09:Bicycle.cpp {O}
// Bicycle implementation
#include "Bicycle.h"
#include <map>
#include <algorithm>
#include <cassert>
using namespace std;

// Static member definitions:
LeakChecker BicyclePart::lc;
int Bicycle::counter = 0;

Bicycle::Bicycle() : id(counter++) {
  BicyclePart *bp[] = {
    new Part<Frame>, 
    new Part<Wheel>, new Part<Wheel>, 
    new Part<Seat>, new Part<HandleBar>,
    new Part<Sprocket>,  new Part<Deraileur>,
  };
  const int bplen = sizeof bp / sizeof *bp;
  parts = VBP(bp, bp + bplen);
}

Bicycle::Bicycle(const Bicycle& old) 
  : parts(old.parts.begin(), old.parts.end()) {
  for(int i = 0; i < parts.size(); i++)
    parts[i] = parts[i]->clone();
}

Bicycle& Bicycle::operator=(const Bicycle& old) {
  purge(); // Remove old lvalues
  parts.resize(old.parts.size());
  copy(old.parts.begin(), 
    old.parts.end(), parts.begin());
  for(int i = 0; i < parts.size(); i++)
    parts[i] = parts[i]->clone();
  return *this;
}

void Bicycle::purge() {
  for(VBP::iterator it = parts.begin();
    it != parts.end(); it++) {
      delete *it;
      *it = 0; // Prevent multiple deletes
  }
}

ostream& operator<<(ostream& os, Bicycle* b) {
  copy(b->parts.begin(), b->parts.end(),
    ostream_iterator<BicyclePart*>(os, "\n"));
  os << "--------" << endl;
  return os;
}

void Bicycle::print(vector<Bicycle*>& vb, 
  ostream& os) {
  copy(vb.begin(), vb.end(),
    ostream_iterator<Bicycle*>(os, "\n"));
  cout << "--------" << endl;
} ///:~

Here’s a test:

//: C09:BikeTest.cpp
//{L} Bicycle
#include "Bicycle.h"
#include <algorithm>
using namespace std;

int main() {
  vector<Bicycle*> bikes;
  BicycleGenerator bg;
  generate_n(back_inserter(bikes), 12, bg);
  Bicycle::print(bikes);
} ///:~


Exercises

  1. Create a heap compactor for all dynamic memory in a particular program. This will require that you control how objects are dynamically created and used (do you overload operator new or does that approach work?). The typically heap-compaction scheme requires that all pointers are doubly-indirected (that is, pointers to pointers) so the “middle tier” pointer can be manipulated during compaction. Consider overloading operator-> to accomplish this, since that operator has special behavior which will probably benefit your heap-compaction scheme. Write a program to test your heap-compaction scheme.
[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]
Last Update:02/22/2000