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:
- Convert an old-style error message to a new one by creating a call to
errorlog( CErrorName( arg1, arg2 ) );
- Errorlog requires a class derived from CError, so create CErrorName as a new CError-derived class in a header file
- 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
- External dependencies are created by DECLARE_LEAF_CLASS, and the simplest way to resolve them is to use IMPLEMENT_LEAF_CLASS
- 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.