Channels ▼
RSS

C/C++

Self-Registering Objects in C++

Source Code Accompanies This Article. Download It Now.


An interesting design limitation with C++ is that all the places in the code that create objects have to hardcode the types of objects that can be created because all the usual methods of creating an object in C++, such as new(classname), require you to specify a concrete type for the class name. This design breaks encapsulation. The situation is understood well by many beginning C++ designers, who try to find a virtual constructor that will create classes without knowing the exact type. These beginners quickly find out that C++ doesn't have such a thing.

Since the functionality isn't built into C++, I will add it by creating a class that can create other classes based on some criteria instead of a concrete type. Classes designed to create other classes are frequently called "factories." I'll call the class described in this article the "specialty store," because it only works with objects that are closely related and it leaves the actual work of creation to other classes.

At compile time, the specialty store has no knowledge of the concrete classes it will be working with, but those concrete classes know about the specialty store. There are two remarkable things about this arrangement: A specialty store doesn't contain a single new statement; and the specialty store's implementation doesn't include the header files for any of the classes that it will create at run time. (Instead, when the specialty store is asked for a new object, it queries the classes it knows how to create, asking each if it is appropriate for the current situation — if so, then that class is asked to create an instance of itself.)

Background

Every C++ programmer is familiar with encapsulation, which basically says that an object is a black box that has a set of defined interfaces, but whose internal workings are hidden. In theory, encapsulation lets you do things like transparently replace an algorithm or data structure. But consider a larger problem: How do you encapsulate the fact that an object exists at all, as with, say, a set of file import filters?

In Listing One(a) the C library code is where the code that supports the file formats is kept. The application code shown in Listing One(b) is what actually uses the library code. All the string handling is kept in one place. The processing is divided into separate functions. The code is easy to understand and new formats can easily be added during development. The problem with this code is that the supported formats are hardcoded into the application. In a typical application, Listing One(b) will be cut-and-pasted several times to different parts of the application's source code. As the application code grows, it becomes much more difficult to support another file format, even if the library code fully supports it.

Listing One(a)

enum file_types { TYPE_UNKNOWN, TYPE_JPEG, TYPE_TIFF, TYPE_GIF };int find_extension_type(char *ext)
{
    if (strcmp(ext, ".JPG") == 0)
        return TYPE_JPEG;
    if (strcmp(ext, ".TIF") == 0)
        return TYPE_TIFF;
    if (strcmp(ext, ".GIF") == 0)
        return TYPE_GIF;
    return TYPE_UNKNOWN;
}

Listing One(b)

int type = find_extension_type(extension);switch(type)
{
case TYPE_JPEG:
    import_jpeg(filename);
    break;
case TYPE_TIFF:
    import_tiff(filename);
    break;
case TYPE_GIF:
    import_gif(filename);
    break;
}

By rewriting this code in C++ as in Listing Two(a and b), you can create objects that encapsulate how to do each form of conversion, then use polymorphism to allow you to interchangeably use classes such as JpegFileConverter, TifFileConverter, and so on. This C++ version solves much of the maintenance problem because it lets you place knowledge of the supported formats entirely within the library. This is not groundbreaking material; it is basic object-oriented design. Unfortunately, there is still a fundamental problem with this code — the supported file formats are still hardcoded in the library.

Listing Two(a)

ConversionObject* CreateConversionObject(char *ext){
    if (strcmp(ext, ".JPG") == 0)
        return new JpegFileConverter;
    if (strcmp(ext, ".TIF") == 0)
        return new TifFileConverter;
    if (strcmp(ext, ".GIF") == 0)
        return new GifFileConverter;
    return NULL;
}

Listing Two(b)

ConversionObject* pConverter =       
CreateConversionObject(extension);
if (pConverter)
    pConverter->Import(filename);
delete pConverter;

"So what?" you say, "That's the library maintainer's problem!" But what if you were the library maintainer. There is no way to add new formats without modifying the source code. If you were distributing the library in object form only, then application developers would be unable to add any new formats they might need. Also, there is no way to extend the library at run time. If an application developer needs to support another type of file format in a new version of an application, he certainly would rather not have to do a new build and a new release. It would be helpful if customers could be shipped a new shared library that would support the new format.

Justification

All of these requirements can be met by using a "specialty store" in which there is no single place in the code at compile time that knows about all supported formats. The list of supported objects is built at run time when each file-format object registers its existence with a specialty-store object.

There are four parts to building a specialty store:

  • Each class that goes in the store will be represented by a proxy class. The proxy knows how to create objects for the store and provides a standard interface for information about the class.
  • You must decide what criteria the specialty store will expose to callers, then implement interfaces for those criteria in the store, in the proxy class, and in the original class.
  • All proxy classes will derive from a common base class so that the specialty store can use them interchangeably. Each proxy class will be implemented as a template that calls static functions in the original class.
  • Proxy classes will be registered automatically at program startup by defining a global variable for each proxy class whose constructor will register the proxy class with the specialty store.

The Proxy Class

Conceptually, what you want to do is have the existence of a particular class registered with the store. However, you don't want to register a particular instance of that class because the store may need many of them, and there may be side effects associated with creating an instance of the class. To solve this problem, you use a proxy class whose job it will be to register, create, and describe the class it represents. All proxy classes will derive from a common abstract base class. Listing Three presents the definition of class FileConverterProxyBase. A specialty store only creates related objects — in this case, file converters. If the same application also needed to create various types of bitmaps, there would be a separate specialty store that would use a separate proxy base class. All proxy implementations would have a constructor and a CreateObject() member function as in Listing Three. The other member functions, such as GetExtension(), are specific to the kinds of objects being created and would be specialized for each proxy base class.

Listing Three

class FileConverterProxyBase{
public:
    FileConverterProxyBase();
    virtual FileConverter* CreateObject() const = 0;
    // Expose criteria here
    virtual char* GetExtension() const = 0;
    virtual bool IsCompressed() const = 0;
};
FileConverterProxyBase::FileConverterProxyBase()
{
     gConverterStore.Register(this);
}

Defining the Criteria

The ProxyBase class defines a standard interface for the specialty store. Each concrete proxy class will be an instance of a template class that derives from FileConverterProxyBase. At run time, the specialty store keeps a list of instances of FileConverterProxy. Listing Four is the definition of the template class.

Listing Four

template <class T>class FileConverterProxy : public FileConverterProxyBase
{
    FileConverter* CreateObject() const
        { return new T; }
    // Member functions in T are static
    virtual char* GetExtension() const
        { return T::GetExtension(); }
    virtual bool IsCompressed() const
        { return T::IsCompressed(); }
};


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video