Handle-Body Idioms

There are situations where two objects are composed to appear as a single object. One object, called the handle, manages the interface, while another object called the body provides the application logic.

class Body
{
   void ServiceA();
   void ServiceB();
   // etc.
   friend class Handle;
};

class Handle
{
public:
   void serviceA() { myBody->serviceA(); }
   void serviceB() { myBody->serviceB(); }
   // etc.
private:
   Body *myBody;
};

Applications

The body is a shared object.

Shared objects arise in several contexts:

1. An object may be too large to be copied such as a CAD-CAM model of a jumbo jet or the text of a long document,

2. having multiple instances of an object may lead to too many objects. For example, if we want to represent individual characters in a document as objects (bodies are called flyweights in this context),

3. the object may represent a unique resource such as a file, communication chanel, or remote server,

4. or the object may be stateless, hence multiple instances aren't necessary such as non-mutable strings.

These objects can be shared by providing clients with unique proxy objects representing the shared object. In this case the shared object is the body, while the proxies are handles.

The body is a remote object.

In distributed client-server applications client and server objects reside on different machines connected by a network. We want to shield client objects from the details of remote communication (sockets, IP addresses, ports, creating connections, etc.) and instead allow clients to communicate with server objects (the body) using ordinary method invocation (RMI). We can achieve this by introducing a client-side proxy (the handle) that encapsulates the low-level communication details.

Division of labor

Sometimes the responsibilities of an object naturally divide into two categorties. In these situations it is sometimes a good idea to separate these responsibilities into two associated objects. In the orthodox canonical form, for example, the handle performs memory management duties whiule the body implements application logic.

The body has the wrong interface.

One solution is to introduce a handle as an adapter and a body as the adaptee.

We want to dynamically extend or reuse the functionality of the body.

There are two competing reuse/extension mechanisms in object oriented programming: inheritance and delegation.

Sometimes inheritance is called white-box reuse because the public and protected members of the base class are visible to the derived class. Delegation is called black-box reuse because the protected members of the delegate are not visible to the delegator.

We want to dynamically separate the interface from the implementation

Several idioms and design patterns use a handle object as the interface and body objects as implementors: strategy, state, visitor, bridge, and envelope-letter to name a few.

The body is not a C++ object.

How do we integrate objects from other languages into a C++ program? Naturally clients want to be shielded from the syntax and semantics of the foreign object. The idea is to encapsulate a reference to the foreign object (the body) in a C++ object called a wrapper (the handle), which also encapsulates the syntax and semantics of the body. Clients only see and use the wrapper. This technique is used (with limited success) in the MFC framework. Before the framework Windows applications were built out of C "objects" called windows. MFC provides C++ wrapper objects that (partially) encapsulate references to window objects. (Unfortunately, MFC uses the term "handle" to refer to a wrapper's "reference" to the window.)

We can take this idea one step further by allowing the body to be an ordinary C-style data structure such as a linked list or a tree. In this case the wrapper manages access to the data structure and provides memory management.

 

Orthodox Canonical Form

Assume we create two handle-body pairs:

Body *b1 = new Body(...), *b2 = new Body(...);
Handle h1(b1), h2(b2);

The picture in memory looks like this:

Now assume we change our minds and reassign h2 to be like h1:

h2 = h1;

The default assignment algorithm in C++ is called member-wise copy. Each field of h1 is copied to the corresponding field of h2, including the pointer b1. The new picture looks like this:

There are two problems. First, we have a memory leak because *b2 was not properly deallocated (remember, b2 was encapsulated by h2, so there are no more active references to *b2.) Second, we have a potential aliasing problem because h2 and h1 both reference the same body. Changes to the body made through h2 will be reflected in h1.

The soulution is to redefine the assignment operator:

Handle& Handle::operator=(const Handle& h)
{
   if (&h != this)
   {
      free(); // delete myBody
      copy(h); // point myBody to a copy *(h.myBody)
   }
   return *this;
}

The picture now looks like this:

Handles can also be copied by copy constructors, again the default algorithm is member-wise copy, hence we must redefine the copy constructor:

Handle::Handle(const Handle& h)
{
   copy(h); // point myBody to a copy of *(h.myBody)
}

(Whenever we define a constructor, we must also define a default constructor, otherwise we may loose the C++ provided default constructor, which is used to initialize arrays of handles.)

We must also provide a destructor that automatically deallocates the body when the handle is deallocated:

Handle::~Handle()
{
   free(); // delete myBody
}

The copy() and free() functions are private member functions that encapsulate the details of coying and deleting bodies. The new declarationg of the Handle class looks like this:

class Handle
{
public:
   Handle();
   Handle(const Handle& h) { copy(h); }
   ~Handle() { free(); }
   Handle& operator=(const Handle& h);
   void serviceA() { myBody->serviceA(); }
   void serviceB() { myBody->serviceB(); }
   // etc.
private:
   Body *myBody;
   void free();
   void copy(const Handle& h);
};

The members shown in bold face are required for every handle class. Coplien refers to these members collectively as the orthodox canonical form (OCF).

 

Smart Pointers

Problem

C pointers are dangerous creatures. Programmers are always dereferencing null pointers, producing the infamous "core dump" message and crashing the program, or programmers change the value of a pointer, forgetting to delete the memory it formerly pointed at, creating a memory leak.

Solution

One solution is to provide programmers with smart pointers that automatically solve these problems. (In this context C pointers are often called "dumb pointers.") A smart pointer encapsulates and manages a dumb pointer, so users never need to worry about new and delete. Here are some sample programs using smart pointers.

Demo 1

We'll need this to get smart pointers:

#include "pointer.h"

Test1 expects a C pointer, but we can pass it smart pointer. Test2 expects a smart pointer but we can pass it a C pointer:

void test1(int* p)
{
   *p = *p + 1; cout << "*p = " << *p << '\n';
}
void test2(Pointer<int> p)
{
   *p = *p + 1; cout << "*p = " << *p << '\n';
}

Notice that main never calls new or delete for smart pointers:

int main()
{
   // create some smart pointers to ints
   Pointer<int> p1(3), p2(4), p3(p2), p4, p5;

   // dereference smart pointers
   cout << "*p1 = " << *p1 << '\n';
   cout << "*p2 = " << *p2 << '\n';
   cout << "*p3 = " << *p3 << '\n';

   // reassign smart pointer, no memory leak!
   p2 = p1;
   cout << "*p2 = " << *p2 << '\n';
   cout << "*p3 = " << *p3 << '\n';

   // automatically convert between smart and dumb pointers
   int* p = new int(57);
   test1(p1);
   test2(p);

   // initialize a null pointer
   p4 = p3;
   //p4.pointAt(10); // this works, too
   cout << "*p4 = " << *p4 << '\n';

   // no core dump!
   cout << "*p5 = " << *p5 << '\n';

   return 0;
}

Output of Demo 1

*p1 = 3
*p2 = 4
*p3 = 4
*p2 = 3
*p3 = 4
*p = 4
*p = 58
*p4 = 4
Error: null pointer dereferenced!

 

Demo 2

In this demo we create smart pointers to Person objects:

class Person
{
public:
   // must allocate some space for default string args
   Person(string nm = "?", string addr = "?", int a = 0)
   {
      name = nm; address = addr; age = a;
   }

   void print(ostream& os = cout)
   {
      os << "name = " << name << '\n';
      os << "address = " << address << '\n';
      os << "age = " << age << '\n';
   }

   // etc.

private:
   string name;
   string address;
   int age;
};

Main() creates four smart pointers to persons, then calls Person::print() using the -> operator:

int main()
{
   Person e("Smith", "123 Main St.", 49);
   
   Pointer<Person>
      p0(e),
      p1(Person("Wong", "99 Gum Lane.", 49)),
      p2(new Person("Jones", "999 Hick Dr.", 22)),
      p3(p2);
   
   p0->print();
   p1->print();
   p2->print();
   p3->print();
   p3 = p1;
   p3->print();

   return 0;
}

Output from demo 2

name = Smith
address = 123 Main St.
age = 49
name = Wong
address = 99 Gum Lane.
age = 49
name = Jones
address = 999 Hick Dr.
age = 22
name = Jones
address = 999 Hick Dr.
age = 22
name = Wong

address = 99 Gum Lane.

age = 49

Implementation (pointer.h)

A smart pointer encapsulates and manages a dumb pointer. In this context the smart pointer is the handle and the dumb pointer is the body. We'll need to use orthodox canonical form. Also, smart pointer will need to be a template:

#include "utils.h" // needed for error()

 

template <class Storable>
class Pointer
{
public:
   // OCF stuff
   Pointer() { ptr = 0; }
   Pointer(const Pointer<Storable>& p) { copy(p); }
   ~Pointer() { free(); }
   Pointer<Storable>&
      operator=(const Pointer<Storable>& p);


   // converts dumb pointers to smart pointers:
   Pointer(Storable* p) { ptr = p; }
   Pointer(const Storable s)
   {
      ptr = new Storable;
      *ptr = s;
   }
   
   // converts smart pointers to dumb pointers:
   operator Storable* () { return ptr; }

   // member functions
   Storable& operator*();
   Storable* operator->();
   bool isNull() { return ptr == 0; }
   void pointAt(const Storable s)
   {
      if (ptr) free();
      ptr = new Storable;
      *ptr = s;
   }
   
private:
   Storable *ptr; // dumb pointer
   void copy( const Pointer<Storable>&);
   void free() { delete ptr; }
};

Dereferencing a null smart pointer prints a useful error message, then gracefully terminates the program:

template <class Storable>
Storable& Pointer<Storable>::operator*()
{
   if (!ptr)
      error("null pointer dereferenced");
   return *ptr;
}

 

template <class Storable>
Storable* Pointer<Storable>::operator->()
{
   if (!ptr)
      error("null pointer dereferenced");
   return ptr;
}

OCF assignment is always the same:

template <class Storable>
Pointer<Storable>&
Pointer<Storable>::operator=(const Pointer<Storable>& p)
{
   if (&p != this)
   {
      free();
      copy(p);
   }
   return *this;
}

The copy member function automatically allocates memory:

template <class Storable>
void Pointer<Storable>::copy(const Pointer<Storable>& p)
{
   ptr = new Storable;
   *ptr = *(p.ptr);
}

 

Vectors

Other Names

smart arrays, safe arrays, dynamic arrays, dynamic linear lists

Problem

Inserting and deleting elements from a linked list is efficient, but accessing random elements is not since the cursor must be reset to the start of the list, then moved step by step to the desired position.

Using a C array to implement a random access store is unsafe since no array bounds checking is done, and changing the size of a C array is impossible. While changing the size of a C pointer to an array is possible, it's awkward, and still no array bounds checking is done.

Solution

A vector encapsulates and manages a C array on the heap. An overloaded [] operator automatically checks index bounds.

 

Demo

#include "vector.h"

 

int main()
{
   Vector<int> v1(3), v2;
   for(int i = 15; i >= 0; i--)
      v1.push_back(i);
   v2 = v1;
   cout << v2 << '\n';
   for(i = 0; i < 4; i++)
   {
      v2.erase(i);
      cout << v2 << '\n';
   }

   cout << v1;
   v1.insert(1, 100);
   cout << v1;

   return 0;
}

 

vector growing
( 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 )

( 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 )

( 14 12 11 10 9 8 7 6 5 4 3 2 1 0 )

( 14 12 10 9 8 7 6 5 4 3 2 1 0 )

( 14 12 10 8 7 6 5 4 3 2 1 0 )

( 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 )
( 15 100 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 )

 

C++ Implementation

A vector encapsulates and manages a pointer to an array on the heap, so we'll need to use orthodox canonical form. Naturally, vector will need to be a template.

#include "utils.h" // needed for error() and _DEBUG

template <class Storable>
class Vector
{
public:
   enum {BLOCK_SIZE = 100};
   Vector(int n = BLOCK_SIZE);
   Vector(const Vector<Storable>& v) { copy(v); }
   ~Vector() { free(); }
   Vector<Storable>&
      operator=(const Vector<Storable>&);
   
   void insert(int i, const Storable& s);
   void erase(int i);
   void push_back(const Storable& s);
   int size() { return _size; }
   Vector<Storable>& operator[](int i);
   friend ostream&
      operator<<(ostream& os, const Vector<Storable>& vec);

private:
   Storable *vals;
   int _size; // = number of storables in vals
   int capacity; // = length of vals
   void copy(const Vector<Storable>&);
   void free() { if (vals) delete[] vals; }
   void grow(); // grows vector by BLOCK_SIZE storables, if needed
};

The basic constructor automatically allocates memory:

template <class Storable>
Vector<Storable>::Vector(int n)
{
      vals = new Storable[n];
      capacity = n;
      _size = 0;
}

 

As always:

template <class Storable>
Vector<Storable>&
Vector<Storable>::operator=(const Vector<Storable>& v)
{
   if (&v != this)
   { // v = v leads to complications
      free();
      copy(v);
   }
   return *this;
}

The copy member function allocates a new array the size of v.vals, then performs a component-wise copy:

template <class Storable>
void Vector<Storable>::copy(const Vector<Storable>& v)
{
   capacity = v.capacity;
   _size = v._size;
   vals = new Storable[capacity];
   for(int i = 0; i < _size; i++)
      vals[i] = v.vals[i];
}

The subscript operator performs index bounds checking:

template <class Storable>
Vector<Storable>& Vector<Storable>::operator[](int i)
{
   if (i < 0 || _size <= i)
      error("index out of range");
   return vals[i];
}

 

If necessary, grow() creates a new array, BLOCK_SIZE storables larger than the old array, copies the old array into the new array, then deletes the old array. Note: this is more efficient than doubling the size of the old array.

template <class Storable>
void Vector<Storable>::grow()
{
   if (capacity <= _size)
   {
      if (_DEBUG) cerr << "vector growing\n";
      capacity += BLOCK_SIZE;
      Storable *temp = new Storable[capacity];
      
      for(int i = 0; i < _size; i++)
         temp[i] = vals[i];

      delete[] vals;
      vals = temp;
   }
}

New items can be added to a vector using insert or push_back. Note that push_back is much more efficient, because insert must move move as many as _size elements.

template <class Storable>
void Vector<Storable>::insert(int i, const Storable& s)
{
   grow();
   // groan!
   for(int j = _size; i < j; j--)
      vals[j] = vals[j - 1];
   vals[i] = s;
   _size += 1;
}

 

template <class Storable>
void Vector<Storable>::push_back(const Storable& s)
{
   grow();
   vals[_size] = s;
   _size += 1;
}

 

Erase is also inefficient, because it may need to move up to _size elements. It's better to replace items using [] rather than erase them.

template <class Storable>
void Vector<Storable>::erase(int i)
{
   if (i < 0 || _size <= i)
      error("index out of range");
   // groan
   for(int j = i; j < _size; j++)
      vals[j] = vals[j + 1];
   _size -= 1;
}

A little print utility:

template <class Storable>
ostream& operator<<(ostream& os, const Vector<Storable>& vec)
{
   os << "( ";
   for(int i = 0; i < vec._size; i++)
      os << vec.vals[i] << ' ';
   os << ")\n";
   return os;
}

 

Counted Pointer Idiom

Other Names

Reference counting

Problem

Orthodox canonical form can be dangerous if the handle ends up deleting a body that is referenced by other handles. This is possible when the body is a shared object. As a general rule, an object should only delete objects it creates. But what if no other objects are referencing the body? How would the handle know this?

Solution

The body can keep count of the number of handles that reference it. Each new handle that references the body should increment this count. Handles should decrement the count just before they disappear. The handle that decrements the count to zero should delete the body.

Implementation

Only handles can construct bodies, because the body constructors are private:

class Body
{
void ServiceA();
void ServiceB();
// etc.
friend class Handle;
Body(...); // possible parameters
~Body();
int count; // = # of handles
};

 

The handle is as above, but the non-copy constructors must allocate a new body and set its reference count to 1:

Handle::Handle(...)
{
myBody = new Body(...);
myBody->count = 1;
}

The free method first decrements the count. If the count has reached 0, then the body must be deleted:

void Handle::free()
{
if (--myBody->count <= 0)
delete myBody;
}

The copy method increments the count, that attaches itself to the body of h:

void Handle::copy(const Handle& h)
{
h.myBody->count += 1;
myBody = h.myBody;
}

 

Example: Non-mutable Strings

As an example, we might implement safe, non-mutable strings using the handle body idiom. A character in a non-mutable string can't be altered. For example, to create the string "cat" from "bat", a new string needs to be created. Java strings are non-mutable.

sstring.h

Here are the includes we need:

#include <string>
#include <iostream>
#include <stdexcept>
using namespace std;
#define DEBUG true

 

StringBody encapsulates a C++ string and provides some basic access services:

class StringBody
{
   StringBody(string str = "?")
   {
      theString = str;
   }

   ~StringBody()
   {
      if (DEBUG)
         cout << theString + " deleted\n";
   }

   // some services
   void print(ostream& os = cout)
   {
      os << theString;
      if (DEBUG)
         os << " (handle count = " << count << ')';
      os << '\n';
   }

   char operator[](int i) // don't return char&
   {
      try
      {
         return theString.at(i);
      }
      catch(out_of_range e)
      {
         cerr << '\n' << e.what() << '\n';
      }
   }

   int size() { return theString.size(); }

   friend class String;
   int count;
   string theString;
};

 

The String class encapsulates a pointer to a StringBody and implements orthodox canonical form:

class String
{
public:
   String(string str = " ")
   {
      body = new StringBody(str);
      body->count = 1;
   }

   String(const String& s) { copy(s); }
   ~String() { free(); }
   String& operator=(const String& s)
   {
      if (this != &s)
      {
         free();
         copy(s);
      }
      return *this;
   }

   void print(ostream& os = cout)
   {
      body->print();
   }

   char operator[](int i)
   {
      return body->operator[](i);
   }

   int size() { return body->size(); }

private:
   StringBody* body;
   void copy(const String& s);
   void free();
};

 

sstring.cpp

Implementations of copy and free are separated from their declarations:

#include "sstring.h"

void String::copy(const String& s)
{
   body = s.body;
   body->count += 1;
}

void String::free()
{
   body->count -= 1;
   if (body->count <= 0)
      delete body;
}

 

test.cpp

Here's a little test harness:

#include "sstring.h"

int main()
{
   String *s1 = new String("Hello, Earth!");
   s1->print();

   String *s2 = new String(*s1);
   s2->print();

   String *s3 = new String("Hello, Neptune!");
   s3->print();

   cout << "\nreassigning neptune\n";

   *s3 = *s1;
   s3->print();

   cout << "\none char at a time:\n";
   for(int i = 0; i <= s1->size(); i++) // intentionally go too far
      cout << (*s1)[i] << ' ';
   cout << '\n';

   cout << "\ndeleting s3\n";
   delete s3;
   s1->print();
   cout << "\ndeleting s2\n";
   delete s2;
   s1->print();
   cout << "\ndeleting s1\n";
   delete s1;

   return 0;
}

Program Output

Hello, Earth! (handle count = 1)
Hello, Earth! (handle count = 2)
Hello, Neptune! (handle count = 1

reassigning neptune
Hello, Neptune! deleted
Hello, Earth! (handle count = 3)

one char at a time:
H e l l o , E a r t h !
invalid string position
ü

deleting s3
Hello, Earth! (handle count = 2)

deleting s2
Hello, Earth! (handle count = 1)

deleting s1
Hello, Earth! deleted

 

Envelope-Letter Idiom

Problem

Clients often regard the lower levels of an inheritance hierarchy as details they don't want to know about. Clients want to deal with generic abstractions like curves and surfaces rather than technicalities like conic sections, Bezier splines, and revolutes.

C++ already provides abstract classes that allow programmers to seperate interface from implementation, but what happens when several generic objects of the same type can have different implementations. For example, a complex number can be represented in rectangular form as a+bi or in polar form as reiq. How do we allow both representations to coexist while the user is blissfully ignorant of which of his complex numbers use polar form and which use rectangular form?

Solution

When the services of a body specialize the services of the handle, we call the body a letter and the handle an envelope. In this case the Handle-Body idiom is called the Envelope-Letter idiom. (Names like "Envelope-Letter" and "Orthodox Canonical Form" come from James Coplien's book Advanced C++ Programming Styles and Idioms.)

The envelope defines a generic type (like curve, surface, or complex number). The envelope encapsulates a pointer to a particular representation, called the letter (like conic section, revolute, or polar). Envelope constructors and factory methods deduce the type of letter to create from their parameters and contexts. Envelope member functions are always virtual and always delegate work to member functions of the same name in the letter class. Thus, the typing system does most of the work for us.

Generic Numbers

There are many types of numbers: integer, rational, real, and complex numbers. Although all types of numbers may be needed by our clients, our clients may only be interested in using generic numbers, leaving the representation details to us on a case by case basis.

#include "number.h"

int main()
{
   Number n1(3, 4), n2(5.0, 6.0);
   cout << n1 << '\n';
   cout << n2 << '\n';

   Number n3 = n1 + n2;
   cout << n3 << '\n';

   Number n4(3.1416);
   Number n5(42);
   cout << n4 << '\n';
   cout << n5 << '\n';

   n3 = n1 + n4;
   cout << n3 << '\n';

   n3 = n2 + n5 + n1;
   cout << n3 << '\n';

   return 0;
}

Test Result

 

3/4

5+6i

5.75+6i

3.1416

42

3.8916

47.75+6i

 

Static Structure

We can use the Envelope-Letter idiom to implement the Number class. In this case Number is the envelope class, while Rational, Complex, Real, and Integer are the letter classes:

 

The interesting feature of the Envelope-Letter idiom is that the letter classes inherits from the envelope class, while the envelope class delegates to the letter class. Here's an object diagram showing the first two numbers created in the example above:

 

Specialization vs. Extension

Class derivation in C++ blurs the distinction between specialization and extension. If class A extends class B, then instances of A contain the attributes from B extended by the attributes from A. An instance of A is a single object, probably allocated in a contiguous block of memory.

If class C specializes class B, then x, an instance of A, contains a reference to y, a seperate instance of B. y represents a generalization of x and x represents a specialization of y. Sometimes we want x, sometimes we want y.

In the Envelope-Letter idiom we have both specialization and extension. Let's name the instance of Rational in the object diagram r1. Although r1 and n1 are distinct objects in the computer's memory, r1 is a logical specialization of n1, and n1 is a logical generalization of r1. Of course Rational extends Number, so number attributes, if any, can be found inside of r1. As such, we can regard r1 as an instance of Number with some added attributes. We can retype r1 as a Number with a cast. Let n = (Number) r1. While some might claim n to be the C++ generalization of r1, it is unrelated to the logical generalization, n1.

 

Dynamic Structure

Assume n1 and n2 are as in the example above. Assume Rational(*(n1.letter)) = r1 is actually an instance of Rational, and Complex(*(n2.letter)) = c2, an instance of Complex. We use the C++ typing system to delegate the calculation of n1 + n2 to c2 + r1:

Note that the actual addition can only be preformed in c2 + r1, where the specific representation of c2 and r1 is known. Also note that our addition operators are not commutative. We use the argument order to determine which variant to call.

 

Implementation

The implementation isn't difficult, but involves writing a lot of code. Most of the member functions merely delegate to the corresponding member functions of the letter. To do this, it is important that these functions are virtual and that letter is a pointer. Otherwise, infinite recursions result.

number.h

You'll need some declarations at the top of number.h:

 

#include <iostream>

using namespace std;

 

class Token {};

 

class Rational;

class Complex;

class Integer;

class Real;

The bad news is that each arithmetic operation (+, *, /, -) requires five member functions in the Number base class. (Total = 4 * 5 = 20.) One to be used when *this is the first operand, the others to be used when *this is the second operand. In this case the parameter type tells us the letter type of the first argument. The good news is that the code of each of the five member functions is exactly the same: return *letter + n; This is because operator+ is heavily overloaded in the derived classes.

 

// the envelope class
class Number
{
public:

   // constructors
   Number(int n, int d); // letter = a Rational
   Number(double rp, double ip);
   Number(int n);
   Number(double d);

   // addition functions
   // hint: all five have the same body!

   // used when this is the first operand
   virtual Number operator+(const Number& n) const;

   // used these when this is the second operand
   virtual Number operator+(const Complex& n) const
   virtual Number operator+(const Rational& n) const
   virtual Number operator+(const Real& n) const
   virtual Number operator+(const Integer& n) const

   // multiplication
   // subtraction
   // division

   friend ostream& operator<<(ostream& os, const Number n);

   virtual void printNumber(ostream& os) const
   {
      letter->printNumber(os);
   }

protected:
   // Used to init base part of *letter.
   Number (Token t) { letter = 0; }

private:
   Number* letter; // points to a Complex, Real, etc.
}; // Number

Note the protected constructor expecting a parameter of type Token, where Token is the bogus empty class defined earlier. This is the constructor used by the derived classes, but never other clients, to create an extension (i.e. not generalization) of type Number. Using the other constructors would result in an infinite regress of specializations creating generalizations, creating specializations, etc.

Complex Numbers

Each derived class must over ride the 4 * 5 = 20 virtual arithmetic operators defined in the Number base class. (There are four derived classes, which means 4 * 20 = 80 member functions! This is a bit much, but it is still a great exercise in C++. Can you think of any short cuts using macros, templates, etc.?)

class Complex: public Number
{
public:
   Complex(double rp, double ip): Number(Token())
   {
      realPart = rp;
      imPart = ip;
   }

   // used when this is the first operand
   Number operator+(const Number& n) const
   {
      return n + *this;
   }

   // used when this is the second operand
   // this is where the math is
   Number operator+(const Complex& x) const;
   Number operator+(const Rational& x) const;
   Number operator+(const Integer& x) const;
   Number operator+(const Real& x) const;

   // Multiplication
   // Division
   // Subtraction

   void printNumber(ostream& os) const
   {
      os << realPart << '+' << imPart << 'i';
   }

   double getImPart() const{ return imPart; }
   double getRealPart() const{ return realPart; }

private:
   double realPart, imPart;
};

The other classes are similar. Rational encapsulates two integers representing the numerator and denominator, Real encapsulates a double, and Integer encapsulates an int:

class Rational: public Number { ... };
class Real: public Number { ... };
class Integer: public Number { ... };

numbers.cpp

Implementation of Number constructors must be seperated from their declaration, because at the time of declaration, we didn't know what type of derived class constructors would be available to us. For example, the Number constructor receiving two ints, n and d, must define letter = new Rational(n, d).

#include "number.h"

 

// define Number constructors here

 

ostream& operator<<(ostream& os, const Number n)
{
   n.printNumber(os);
   return os;
}

Recall that the actual mathematics can only be implemented when the derived types of both arguments are known. Here's an example. At this point we know the implicit parameter is complex and the explicit parameter is rational. In this case we must transform the rational x into a double by dividing its numerator by its denominator. The result, x2, is added to the real part of the implicit parameter. The answer is a new complex number made this sum and the imaginary part of the implicit parameter. (Adding a real, rational, or int to a complex number doesn't effect its imaginary part.)

Number Complex::operator+(const Rational& x) const
{
   double x2 = double(x.getNum())/double(x.getDen());
   return Number(realPart + x2, imPart);
}

Project

Complete the implementation of generic numbers. You don't have to implement division and subtraction, but do finish addition and multiplication.

Create a test harness (main) to test your implementation. The test harness should only refer to the Number type, never the derived types.

Improvements

1. Supply a reader for the Number class. A reader is just a global function of the form:

istream& operator>>(istream& is, Number& n) { ... }

Use some of the tricks used in utils.cpp to determine from the contents of is what type of letter to create.

2. The Number class should use orthodox canonical form to manage its letter pointer, but how can the copy member function be implemented:

void Number::copy(const Number& n) { ??? }

if the derived type of the letter is unknown? One ide is to make the copy function virtual, and over ride it in the derived classes. Then Number::copy simply calls n.letter->copy().

3. For testing purposes, don't worry about reducing rational numbers. If you have time, you should reduce rational numbers in the rational number constructors and readers. Here's a useful member function that reduces and normalizes its rational implicit parameter:

void Rational::reduce()
{
   if (den == 0)
      error("denominator = 0");
   int s = sign(num * den);
   int n1 = abs(num);
   int d1 = abs(den);
   int q = gcd(n1, d1);
   num = s * n1/q;;
   den = d1/q;
}

gcd() is an efficient tail recursion that uses an Euclidean algorithm to find the greatest common divisor of n and m:

int gcd(int n, int m)
{
   if (n < 0 || m < 0)
      return gcd(abs(n), abs(m));
   if (n < m)
      return gcd(m, n);
   if (m == 0)
      return n;
   else
      return gcd(m, n % m);
}

sign() returns 1 if 0 <= n and –1 otherwise:

inline int sign(int n)
{
   return n < 0? -1: 1;
}

4. It might simplify things to supply constructors and overloaded casting operators between our derived number types and the corresponding C++ types. For example, we could define

Rational::operator double()
{
   return double(x.getNum())/double(x.getDen());
}

Then Complex::operator+ is simply:

Number Complex::operator+(const Rational& x) const
{
   return Number(realPart + double(x), imPart);
}

We don't even need the explicit cast. C++ will provide it for us automatically!

5. The low level arithmetic functions should be improved so they return the simplest type answer possible. For example (3.0 + 4.0i) * (3.0 + -4.0i) should return the integer 25.