Dynamic Instantiation in C++ (The Prototype Pattern)

Unfortunately, C++ doesn't have built-in support for dynamic instantiation, but the prototype pattern provides a standard way to add this feature to C++ programs:

Prototype [Go4]

Problem

A "factory" class can't anticipate the type of "product" objects it must create.

Solution

Derive all product classes from an abstract Product base class that declares a pure virtual clone() method. The Product base class also functions as the product factory by providing a static factory method called makeProduct(). This function uses a type description parameter to locate a prototype in a static prototype table maintained by the Product base class. The prototype clones itself, and the clone is returned to the caller.

Static Structure

We saw that Java and MFC both required some common base class for all classes that can be dynamically instantiated: Object in Java and CObject in MFC. The Prototype Pattern also requires a common base class, which we call Product. In addition to serving as the base class for all products, the Product class maintains a static table that holds associations between names of Product-derived classes ("Product1" for example) and a corresponding prototypical instance of the class. It also provides a static function for adding new associations to the prototype table (addPrototype), and a static factory method for creating products (makeProduct). Finally, Product is an abstract class because it contains a pure virtual clone() method. The following diagram shows the Product class with three Product-derived classes.

Test Program

Before describing the implementation, let's look at a test program. Our test driver, main(), begins by displaying the prototype table. It then enters a perpetual loop that prompts the user for the name of a product type, uses a static factory method to instantiate the type entered, then uses the RTTI feature of C++ to display the type of product created:

int main()
{
   string description;
   Product* p = 0;
   cout << "Prototype Table:\n";
   cout << Product::protoTable << endl;
   while(true)
   {
      cout << "Enter the type of product to create: ";
      cin >> description;
      p = Product::makeProduct(description);
      cout << "Type of product created = " ;
      cout << typeid(*p).name() << endl;
      delete p;
   }
   return 0;
}

Program Output

Curiously, the test program produces output before main() is called:

adding prototype for Product1
done
adding prototype for Product2
done
adding prototype for Product3
done

Apparently, prototypes of three product classes have been added to the prototype table. This is confirmed when the prototype table is displayed at the beginning of main(). It shows pairs consisting of the product name and the address of the corresponding prototype:

Prototype Table:
{
(Product1, 0xba1cc)
(Product2, 0xba1dc)
(Product3, 0xba1ec)
}

Now the loop begins. The user creates instances of each of the product classes, as confirmed by runtime type identification. (We are using the DJGPP compiler in this example.) When the user attempts to create an instance of an unknown class, an error message is displayed and the program terminates:

Enter the type of product to create: Product1
Type of product created = 8Product1
Enter the type of product to create: Product2
Type of product created = 8Product2
Enter the type of product to create: Product3
Type of product created = 8Product3
Enter the type of product to create: Product4
Error, prototype not found!

The Product Base Class

The main job of the Product base class is to maintain the prototype table. Since the prototype table contains associations between type names (string) and the addresses of corresponding prototypes (Product*), we declare it as a static map of type:

map<string, Product*>

The Product base class also provides a static factory method for dynamically creating products (makeProduct) and a static method for adding prototypes to the prototype table (addPrototype). Lastly, the Prototype Pattern requires that each product knows how to clone itself. This can be enforced by placing a pure virtual clone method in the Product base class (this idea is related to the Virtual Body Pattern in Chapter 4). Here's the declaration:

class Product
{
public:
   virtual ~Product() {}
   virtual Product* clone() const = 0;
   static Product* makeProduct(string type);
   static Product* addPrototype(string type, Product* p);
   static map<string, Product*> protoTable;
};

Recall that the declaration of a static class variable, like protoTable, is a pure declaration that simply binds a name (protoTable) to a type (map<>). No variable is actually created. Instead, this must be done with a separate variable definition. Assuming the Product class is declared in a file called product.h, we might want to place the definition of the prototype variable at the top of the file product.cpp:

// product.cpp
#include "product.h"
map<string, Product*> Product::protoTable;

Our product.cpp implementation file also contains the definitions of the makeProduct() factory method and the addPrototype() function.

The makeProduct() factory method uses the global find() function defined in util.h (which is listed in Appendix 3 and should be included at the top of product.h) to search the prototype table. The error() function defined in util.h is used to handle the error if the search fails. Otherwise, the prototype located by the search is cloned, and the clone is returned to the caller:

Product* Product::makeProduct(string type)
{
   Product* proto;
   if (!find(type, proto, protoTable))
      error("prototype not found");
   return proto->clone();
}

The addPrototype() function has two parameters representing the name of a Product-derived class ("Product1" for example) and a pointer to a prototypical instance of that class. The function simply adds a new association to the prototype table. For debugging purposes, the statement is sandwiched between diagnostic messages. If for some reason we fail to add a particular prototype to the prototype table, we will know exactly which one caused problems. (More on this later.) Finally, notice that the prototype pointer is returned. The purpose of this return statement will also be explained later.

Product* Product::addPrototype(string type, Product* p)
{
   cout << "adding prototype for " << type << endl;
   protoTable[type] = p;
   cout << "done\n";
   return p; // handy
}

 

Creating Product-Derived Classes

One measure of quality for a framework is how easy it is to customize. Frameworks with heavy overhead (i.e., that require customizers to write hundreds of lines of code beyond what they already have to write) are often very unpopular. How much extra work is it to derive a class from our Product base class? Only four extra lines of code are required.

Assume we want to declare a class named Product1 in a file named product1.h. We want to be able to dynamically instantiate Product1, so we must derive it from the Product base class. The bold face lines show the overhead imposed by the Product base class:

// product1.h
#include "product.h"

class Product1: public Product
{
public:
   IMPLEMENT_CLONE(Product1)
   // etc.
};

Assume the Product1 class is implemented in a file named product1.cpp. We must add a single line to that file, too:

// product1.cpp
#include "product1.h"
MAKE_PROTOTYPE(Product1)
// etc.

Macros

IMPLEMENT_CLONE() and MAKE_PROTOTYPE() are macros defined in product.h. Recall that macro calls are expanded by the C pre-processor before compilation begins. For example, if a programmer defines the following macro:

#define PI 3.1416

All calls or occurrences of PI in a program are replaced by the value 3.1416.

Macros can also have parameters. In this case an argument is specified when the macro is called, and the expansion process automatically substitutes the argument for all occurrences of the corresponding parameter in the macro's body.

For example, each Product-derived class must implement the pure virtual clone() function specified in the Product base class. In fact, the implementations are simple and won't vary much from one class to the next. There is a risk, however, that programmers might get too creative and come up with an implementation that's too complex or just plain wrong.

To reduce this risk, we provide the IMPLEMENT_CLONE() macro, which is parameterized by the type of product to clone. The macro body is the inline implementation of the required clone function:

#define IMPLEMENT_CLONE(TYPE) \
   Product* clone() const { return new TYPE(*this); }

(Notice that macro definitions the span multiple lines use a backslash character as a line terminator.)

We placed a call to this macro in the declaration of the Product1 class:

class Product1: public Product
{
public:
   IMPLEMENT_CLONE(Product1)
   // etc.
};

After pre-processor expands this call the declaration of Product1 will look like this:

class Product1: public Product
{
public:
   Product* clone() const {return new Product1(*this); }
   // etc.
};

Notice that the TYPE parameter in the macro body has been replaced by the Product1 argument, forming a call to the Product1 copy constructor. Readers should verify that the implementation correctly returns a clone of the implicit parameter.

Creating Prototypes

The output produced by our test program showed three prototypes were created and added to the prototype table before main() was called. How was that done? In general, how can programmers arrange to have code executed before main()? Isn't main() the first function called when a C++ program starts?

Actually, we can arrange to have any function called before main(), provided that function has a return value. For example, the function:

int hello()
{
   cout << "Hello, World\n";
   return 0; // a bogus return value
}

will be called before main() if we use its return value to initialize a global variable:

int x = hello(); // x = a bogus global

This can be verified by placing a diagnostic message at the beginning of main() and observing that the "Hello, World" message appears first.

Recall that the addPrototype() function returned a pointer to the prototype. If we use this return value to initialize a bogus global Product pointer variable:

Product* Product1_myPrototype =
   Product::addPrototype("Product1", new Product1());

then the call to addPrototype() will precede the call to main(). In principle, we can build the entire prototype table before main() is called.

Our MAKE_PROTOTYPE() macro expands into definitions like the one above:

#define MAKE_PROTOTYPE(TYPE) \
   Product* TYPE ## _myProtoype = \
      Product::addPrototype(#TYPE, new TYPE());

During expansion, the macro parameter, TYPE, will be replaced by the macro argument, Product1 for example, in three places. First, the ## operator is used to concatenate the type name with _myPrototype. In our example this produces Product1_myPrototype, a (hopefully) unique name for a global variable.

Second, the # operator is used to stringify the argument.  If the argument is Product1, #TYPE will be replaced by "Product1", the string name of the type.

Finally, the last occurrence of the TYPE parameter will be replaced by a call to the default constructor specified by the argument.

Linker Issues

Adding entries to the prototype table before main() is called is risky. Recall that the definition of the prototype table occurs in product.cpp, while the calls to the MAKE_PROTOTYPE() macro occur in the files product1.cpp, product2.cpp, and product3.cpp. These files will compile into the object files product.o, product1.o, product2.o, and product3.o, respectively, which will be linked with main.o by the linker (ld). The actual link command might look like this:

ld demo main.o product.o product1.o product2.o product3.o

Suppose the order of object file arguments passed to the linker is modified:

ld demo main.o product1.o product.o product2.o product3.o

In the executable image produced by the linker, the declaration:

Product* Product1_myPrototype =
   Product::addPrototype("Product1", new Product1());

contained in product1.o may precede the creation of the prototype table contained in product.o:

map<string, Product*> Product::protoTable;

Since the call to addPrototype() attempts to install a pair into the prototype table, this will result in a mysterious program crash that defines most debuggers, because the problem occurs before main() is called. (In our case this bug will be easy to catch thanks to the diagnostic messages printed by addPrototype.)

The problem is easily rectified if we abandon the idea of adding entries to the prototype table before main() is called. In this case the macro calls would be made at the top of main():

int main()
{
   MAKE_PROTOTYPE(Product1)
   MAKE_PROTOTYPE(Product2)
   MAKE_PROTOTYPE(Product3)
   // etc.
}