Skip to content

C++ Runtime Types: Find All Classes Derived From a Base Class (3)

Working on a way to find all the derived classes of a base class: part 1 is here, discussing the motivation and frame of the problem.  Part 2 is here, discussing one approach that works but not for me.

The problem is laziness: if I am in a hurry to put in a new error message and can build a version and send it to testing without updating the error class registry, then eventually I will do it.  So how can we use laziness to our advantage?  How can we make having a consistent error registry the easiest way to get the code to compile?

I found an article on codeproject that also provides a framework for runtime derived-class discovery.  It’s substantially uglier than the meatspace solution quoted in part 2, but it addresses all of the weaknesses: index reversal, multiple hierarchies, and most importantly (for me) compile-time error if a derived class is not registered.  What we’re going to wind up with is this:


class CError: {
DECLARE_ROOT_CLASS(CError);
public:
  // body as in part 1
};

// error logging function
void errorlog( const CError& );

class CDerivedError: {
DECLARE_LEAF_CLASS(CError);
public:
  CUnableToOpenFile() { Init( L"sample.txt", 2 ); }
  CUnableToOpenFile( const wchar_t * psPath,
       DWORD dwError = GetLastError() )
  { Init( psPath, dwError ); }
  format_type Format() const
  { return "Unable to open file {0}: {1}"; }
};

Later on, in the implementation file for the error system:


IMPLEMENT_ROOT_CLASS(CError)

IMPLEMENT_LEAF_CLASS(CError,CUnableToOpenFile)
IMPLEMENT_LEAF_CLASS(CError,CBadMagicNumber)
// etc.

Why does this help? Why does it make any difference at all? By designing the system this way, there is now a chain of compile-time or link-time — at any case, not runtime — dependencies that make maintaining a consistent error table the easiest way, the laziest way, to add an error.

So under the hood, what are the macros doing? DECLARE_ROOT_CLASS() is the heaviest. It declares static member functions on CError, and it declares a vector of class factories as a static member variable. (Notice that it does not use the elegant Singleton pattern that the meatspace solution did, and I might change that.) It also declares a pure virtual function on CError — GetClassID().

IMPLEMENT_ROOT_CLASS() is mechanical, it just provides the implementation of the factory-adding mechanism.

DECLARE_LEAF_CLASS() is an interesting beast. It declares a static class variable. It implements two functions that are necessary to make CDerived a concrete class: it provides an implementation for GetClassID(), and it provides an implementation for CreateObject(), required by the class factory template. If I remove DECLARE_LEAF_CLASS, I get these errors:

errorlog.cpp(50) : error C2039: ‘CreateObject’ : is not a member of ‘CUnableToOpenFile’

e\ unabletoopenfile.h(3) : see declaration of ‘CUnableToOpenFile’

errorlog.cpp(50) : error C2259: ‘CUnableToOpenFile’ : cannot instantiate abstract class due to following members:

e\unabletoopenfile.h(3) : see declaration of ‘CUnableToOpenFile’

errorlog.cpp(50) : warning C4259: ‘int __thiscall CError::ClassID(void) const’ : pure virtual function was not defined errorlog.h(31) : see declaration of ‘ClassID’

And  IMPLEMENT_LEAF_CLASS() defines the static class variable that was declared by DECLARE_LEAF_CLASS().  The initialization of that static variable (at runtime, before main() executes) causes the class to be registered in CError’s factory table.  So if I create and use an error class but forget to use the IMPLEMENT macro, I get the following errors:

File.obj : error LNK2001: unresolved external symbol “private: static class CBootStrapper<class CError> CUnableToOpenFile::s_oBootStrapperInfo” (?s_oBootStrapperInfo@CUnableToOpenFile@@0V?$CBootStrapper@VCError@@@@A)

.debug/program.exe : fatal error LNK1120: 1 unresolved externals

To recap: there is a chain of compile-time or link-time errors that ensure that when a new error class is created, it is available through the runtime error-class table maintained in CError.  The chain runs like this:
  1. Convert an old-style error message to a new one by creating  a call to errorlog( CErrorName( arg1, arg2 ) );
  2. Errorlog requires a class derived from CError, so create CErrorName as a new CError-derived class in a header file
  3. In order to derive from CError, we must supply bodies for pure virtual functions declared in CError; the simplest way to do that is with DECLARE_LEAF_CLASS
  4. External dependencies are created by DECLARE_LEAF_CLASS, and the simplest way to resolve them is to use IMPLEMENT_LEAF_CLASS
  5. IMPLEMENT_LEAF_CLASS causes the class to be registered in the error table.
There are other design issues (like, “Why did the derived error class suddenly get a default constructor?”) which I hope to address in a later article; also there are other design considerations about the error logging, the choice of fastformat, “fun” “quirks” of fastformat, getting the errors into a useful database in a useful way (in this case it means using sqlite and possibly using an ORM), which will come up later.
I also made some changes to the original version of BootStrap.h which I hope to publish soon.  I’m not yet completely happy with the templates and macros that I’m using, but I haven’t figured out what I want to change yet.