I l@ve RuBoard Previous Section Next Section

8.7 Clone Factories

Although genetic factories producing clones of the universal soldier are quite a scary prospect, cloning C++ objects is a harmless and useful activity most of the time. Here the goal is slightly different from what we have dealt with so far: We no longer have to create objects from scratch. We have a pointer to a polymorphic object, and we'd like to create an exact copy of it. Because we don't exactly know the type of the polymorphic object, we don't exactly know what new object to create, and this is the actual issue.

Because we do have an object at hand, we can apply classic polymorphism. Thus, the usual idiom used for object cloning is to declare a virtual Clone member function in the base class and to have each derived class override it. Here's an example using our geometric shapes hierarchy:



class Shape


{


public:


   virtual Shape* Clone() const = 0;


   ...


};





class Line : public Shape


{


public:


   virtual Line* Clone() const


   {


      return new Line(*this);


   }


   ...


};


The reason that Line::Clone does not return a pointer to Shape is that we took advantage of a C++ feature called covariant return types. Because of covariant return types, you can return a pointer to a derived class instead of a pointer to the base class from an overridden virtual function. From now on, the idiom goes, you must implement a similar Clone function for each class you add to the hierarchy. The contents of the functions are the same: You create a Polygon, you return a new Polygon(*this), and so on.

This idiom works, but it has a couple of major drawbacks:

  • If the base class wasn't designed to be cloneable (didn't declare a virtual function equivalent to Clone) and is not modifiable, the idiom cannot be applied. This is the case when you write an application using a class library that requires you to derive from its base classes.

  • Even if all the classes are changeable, the idiom requires a high degree of discipline. Forgetting to implement Clone in some derived classes will remain undetected by the compiler and may cause runtime behavior ranging from bizarre to pernicious.

The first point is obvious; let's discuss the second one. Imagine you derived a class DottedLine from Line and forgot to override DottedLine::Clone. Now say you have a pointer to a Shape that actually points to a DottedLine, and you invoke Clone on it:



Shape* pShape;


...


Shape* pDuplicateShape = pShape->Clone();


The Line::Clone function will be invoked, returning a Line. This is a very unfortunate situation because you assume pDuplicateShape to have the same dynamic type as pShape, when in fact it doesn't. This might lead to a lot of problems, from drawing unexpected types of lines to crashing the application.

There's no solid way to mitigate this second problem. You can't say in C++: "I define this function, and I require any direct or indirect class inheriting it to override it." You must shoulder the painful, repetitive task of overriding Clone in every shape class, and you're doomed if you don't.

If you agree to complicate the idiom a bit, you can get an acceptable runtime check. Make Clone a public nonvirtual function. From inside it call a private virtual function called, say, DoClone, and then enforce the equality of the dynamic types. The code is simpler than the explanation:



class Shape


{


   ...


public:


   Shape* Clone() const//nonvirtual


   {


      // delegate to DoClone


      Shape* pClone = DoClone();


      // Check for type equivalence


      // (could be a more sophisticated test than assert)


      assert(typeid(*pClone) == typeid(*this));


      return pClone;


   }


private:


   virtual Shape* DoClone() const = 0; // private


};


The only downside is that you can no longer use covariant return types.

Shape derivees would always override DoClone, leave it private so that clients cannot call it, and leave Clone alone. Clients use Clone only, which performs the runtime check. As you have certainly figured out, programming errors, such as overriding Clone or making DoClone public, can still sneak in.

Don't forget that, no matter what, if you cannot change all the classes in the hierarchy (the hierarchy is closed) and if it wasn't designed to be cloneable, you don't have any chance of implementing this idiom. This is quite a dismissive argument in many cases, so we should look for alternatives.

Here a special object factory may be of help. It leads to a solution that doesn't have the two problems mentioned earlier, at the cost of a slight performance hit—instead of a virtual call, there is a map lookup plus a call via a pointer to a function. Because the number of classes in an application is never really big (they are written by people, aren't they?), the map tends to be small, and the hit should not be significant.

It all starts from the idea that in a clone factory, the type identifier and the product have the same type. You receive as a type identifier the object to be duplicated and pass as output a new object that is a copy of the type identifier. To be more precise, they're not quite the same type: A cloning factory's IdentifierType is a pointer to AbstractProduct. The exact deal is that you pass a pointer to the clone factory, and you get back another pointer, which points to a cloned object.

But what's the key in the map? It can't be a pointer to AbstractProduct because you don't need as many entries as the objects we have. You need only one entry per type of object to be cloned, which brings us again to the std::type_info class. The type identifier passed when the factory is asked to create a new object is different from the type identifier that's stored in the association map, and that makes it impossible for us to reuse the code we've written so far. Another consequence is that the product creator now needs the pointer to the object to be cloned; in the factory we created earlier from scratch, no parameter was needed.

Let's recap. The clone factory gets a pointer to an AbstractProduct. It applies the typeid operator to the pointed-to object and obtains a reference to a std::type_info object. It then looks up that object in its private map. (The before member function of std::type_info introduces an ordering over the set of std::type_info objects, which makes it possible to use a map and perform fast searches.) If an entry is not found, an exception is thrown. If it is found, the product creator will be invoked, with the pointer to the AbstractProduct passed in by the user.

Because we already have the Factory class template handy, implementing the CloneFactory class template is a simple exercise. (You can find it in Loki.) There are a few differences and new elements:

  • CloneFactory uses TypeInfo instead of std::type_info. The class TypeInfo, discussed in Chapter 2, is a wrapper around a pointer to std::type_info, having the purpose of defining proper initialization, assignment, operator==, and operator<, which are all needed by the map. The first operator delegates to std::type_info::operator==; the second operator delegates to std::type_info::before.

  • There is no longer an IdentifierType because the identifier type is implicit.

  • The ProductCreator template parameter defaults to AbstractProduct* (*)(Abstract-Product*).

  • The IdToProductMap is now AssocVector<TypeInfo, ProductCreator>.

The synopsis of CloneFactory is as follows:



template


<


   class AbstractProduct,


   class ProductCreator =


      AbstractProduct* (*)(AbstractProduct*),


   template<typename, class>


      class FactoryErrorPolicy = DefaultFactoryError


>


class CloneFactory


{


public:


   AbstractProduct* CreateObject(const AbstractProduct* model);


   bool Register(const TypeInfo&,


      ProductCreator creator);


   bool Unregister(const TypeInfo&);


private:


   typedef AssocVector<TypeInfo, ProductCreator>


      IdToProductMap;


   IdToProductMap associations_;


};


The CloneFactory class template is a complete solution for cloning objects belonging to closed class hierarchies (that is, class hierarchies that you cannot modify). Its simplicity and effectiveness stem from the conceptual clarifications made in the previous sections and from the runtime type information that C++ provides through typeid and std::type_info. Had RTTI not existed, clone factories would have been much more awkward to implement—in fact, so awkward that putting them together wouldn't have made much sense in the first place.

    I l@ve RuBoard Previous Section Next Section