Self-Registering Objects in C++

The "specialty store" Jim presents lets new classes be registered at run time, even if they live in a shared library or DLL. Once registered, these new classes are treated exactly like built-in classes.


August 01, 1998
URL:http://www.drdobbs.com/cpp/self-registering-objects-in-c/184410633

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:

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(); }
};

All functions in Listing Four are completely defined. The proxy class does not need to be modified for each class that it represents. Both GetExtension() and IsCompressed() relegate their work to the original class. Therefore, the encapsulation is complete. Only class T knows what file extension goes with a particular kind of file type, or if that file type is compressed. (I'll ignore the case where one file type has many extensions. GetExtension() could just as easily have been GetListOfExtensions(), but doing so would have complicated this example.)

The Specialty Store

The design of the specialty store is straightforward. The complete source can be found in class FileConverterStore in Store.h. There is one function named Register() that is called by the constructor for FileConverterProxyBase. There are also several functions that comprise the public interface to the specialty store. These functions expose the available criteria for the registered objects.

The specialty store class has two important restrictions. First, a specialty store must be a global variable so that the proxy classes can find it during program startup. Second, neither the specialty store nor any of its members can have any constructors that initialize member variables. Instead, the specialty store must rely on the linker/loader setting the contents to zero. This restriction arises because the ordering of construction of global objects in C++ is not guaranteed. Because proxy classes are registered during the constructors of global variables, Register() can be called by those constructors before the constructor for the specialty store is called. A constructor for the specialty store could end up writing over entries that had already been legally added to the list. I'll work around this problem by relying on a pointer being set to NULL at load time, then Register will allocate the collection class the first time that it is called.

Putting it all Together

Using a specialty store is easy. Back in Listing Two, I showed a typical implementation of file converters that uses a single abstract base class with one derived class for each converter. With a little extra work, we can add support for a specialty store.

First, you use FileConverterStore gConverterStore; to declare the specialty store as a global variable in your application program. Next, you add support for the criteria functions to each file converter class, as in the class definition in Listing Five. Finally, to register each file converter class with the specialty store, you declare a single instance of the proxy class in its implementation file as shown at the end of Listing Five.

Listing Five

class TiffFileConverter : public FileConverter{
public:
    virtual void Import(const char *filename);
    virtual void Export(const char *filename);
    // These are the criteria support functions
    static char* GetExtension()
        { return ".tif"; }
    static bool IsCompressed()
        { return TRUE; }
};
FileConverterProxy<TiffFileConverter> gTiffProxy;

This declaration is what makes a self-registering object. By including it in the implementation file for TiffFileConverter, the class automatically becomes available at run time without making any changes to any other sources. If necessary, the constructor for FileConverterProxy could be modified to decide at run time whether or not the class should be registered. For example, in the case of TiffFileConverter, a license file could be used to determine if the class should be registered.

Opening for Business

The specialty store can be used in two ways. In the first, the store returns an object based on some criteria. The CreateByExtension() member function creates a conversion object to handle files with a particular extension. Listing Six shows its definition and usage.

Listing Six

// DefinitionFileConverter* FileConverterStore::CreateByExtension(
                   const char* ext)
{
    for (unsigned i=0; i< m_pConverters->size(); i++)
    if (stricmp( m_pConverters->at(i)->GetExtension(),
                ext ) == 0)
        return m_pConverters->at(i)->CreateObject();
    return NULL;
 }

// Usage
void SomeFunc()
{
    FileConverter* pConverter;
    pConverter = gConverterStore.CreateByExtension(".TIF");
    if (pConverter)
        pConverter->Import("Image.TIF");
    delete pConverter;
}C++ LOCALES

The code in Listing Six works almost exactly like the CreateConversionObject() function, except now the list of conversion objects is built dynamically at run time.

To support shoppers who are "just browsing," the store has two member functions, GetCount() and GetAt(), which allow indexed retrieval of each of the available converters; these are defined in Store.cpp (available electronically). As shown in Main.cpp, these functions can be used to write a function that prints the extensions of all compressed formats.

Summary

The specialty store example shows how you can tightly encapsulate objects so that the existence of the class is hidden from other classes at compile time. Objects are created based on one or more criteria, instead of on a concrete name. This mechanism allows new classes to be registered at run time, even if they live in a shared library or DLL. Once registered, these new classes will be treated exactly the same as any built-in classes. (The sample code that's available electronically was written using Visual C++ 4.2. I've also tested it with Visual C++ 5.0. It does not require MFC, but does use STL.)

The specialty store is made up of the store itself, which is essentially an object registry, a set of related classes (such as the conversion objects used in this article), and a set of proxy objects, which are instantiations of template classes that go in the object registry and represent the classes that can be created. The proxy classes provide a common interface for publicizing the criteria that defines the objects.

Specialty stores are particularly useful in situations where the target environment is highly variable, such as programs that rely heavily on networking protocols. Any particular system may support many different protocols. With a specialty store, all of the protocols can be analyzed at run time and only those supported by the local configuration will be installed.


Jim is the author of Multithreading Applications in Win32 (Addison-Wesley, 1997) and a technical leader for Turning Point Software. He can be reached at [email protected].

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.