The C preprocessor is a dangerous weapon when used indiscriminately. In the case of our C++ Visitors, however, some preprocessor magic will provide us with the following benefits:
- Compiler errors (not warnings) when a concrete Visitor class has not been defined correctly.
- Automatic generation of tedious parts of the Default Visitor implementation.
- Linker errors when Visitors which must not implement Default Visitor are not updated to visit a newly-added class.
#include
directives for each concrete visited class, then we won't have to edit Visitor.hpp or Visitor.cpp when we add classes to the hierarchy in the future. Nifty!Let's call the first header file
VisitorSpecs.hpp
. It looks like this:
// NOTE: No include guard for this header!
#ifndef VISITED_ABSTRACT_CLASS
#define VISITED_ABSTRACT_CLASS(cls,parent)
#endif
#ifndef VISITED_ROOT_CLASS
#define VISITED_ROOT_CLASS(cls) \
VISITED_ABSTRACT_CLASS(cls,0)
#endif
VISITED_ROOT_CLASS(Expression)
VISITED_CLASS(Variable, Expression)
VISITED_CLASS(Number, Expression)
VISITED_CLASS(Operation, Expression)
VISITED_ROOT_CLASS(Statement)
VISITED_CLASS(Assignment, Statement)
VISITED_ABSTRACT_CLASS(MultiStatement, Statement)
VISITED_CLASS(Block, MultiStatement)
VISITED_CLASS(IfElse, MultiStatement)
#undef VISITED_ROOT_CLASS
#undef VISITED_ABSTRACT_CLASS
#undef VISITED_CLASS
Hang on a second... This defines a couple of macros as no-ops, and leaves another macro undefined. What gives? The answer is that Visitor.hpp defines the macros one way before including VisitorSpecs.hpp, and Visitor.cpp defines the macros another way, and also includes VisitorSpecs.hpp. That's why the header must not have an include guard: it will get included into Visitor.cpp multiple times. You may even find other consumers for VisitorSpecs.hpp, as you'll see a little later.
The macros are going to end up a little bit complicated, to provide some of the wonderful compiler fail-safes I've been hyping. But to make things clear, let's present the basic implementation first. Visitor.hpp has to include VisitorSpecs.hpp two separate times. First to declare the class names:
#define VISITED_CLASS(cls,parent) class cls;
#define VISITED_ABSTRACT_CLASS(cls, parent) \
VISITED_CLASS(cls,parent)
#include "VisitorSpecs.hpp"
Then to declare the member functions, Visitor.hpp defines the macros as follows:
#define VISITED_CLASS(cls,parent) \
virtual bool enter(const cls &); \
virtual void exit(const cls &);
#define VISITED_ABSTRACT_CLASS(cls, parent) \
VISITED_CLASS(cls,parent)
#include "VisitorSpecs.hpp"
Remember, the "root" classes (those with no parent) are defined in terms of abstract classes. So in fact, Visitor.hpp declares
enter()
and exit()
for every visited class.Visitor.cpp defines default implementations for the member functions by defining the macros like this:
// No parent default: don't recurse, do nothing.
#define VISITED_ROOT_CLASS(cls) \
bool Visitor::enter(const cls &) \
{ \
VISITOR_CATCH_MISSING_HEADER(cls); \
return false; \
} \
void Visitor::exit(const cls &) \
{ \
}
// Child classes: do what the parent would do.
#define VISITED_CLASS(cls,parent) \
bool Visitor::enter(const cls &c) \
{ \
VISITOR_CATCH_MISSING_HEADER(cls); \
const parent &p = c; \
return enter(p); \
} \
void Visitor::exit(const cls &c) \
{ \
const parent &p = c; \
exit(p); \
}
// Abstract classes: same as for concrete classes.
#define VISITED_ABSTRACT_CLASS(cls, parent) \
VISITED_CLASS(cls,parent)
#include "VisitorSpecs.hpp"
The macro definitions for Visitor.cpp implement the Default Visitor scheme: if a concrete Visitor doesn't define enter() for a given type, the default is picked up from the base class, and looks in the virtual table for the enter() belonging to the parent class. We have Visitor.cpp include the header file VisitedClasses.hpp, which includes the .hpp for every concrete visited class. Thus, when a new class is added to the hierarchy, Visitor.hpp and Visitor.cpp do not require any change whatsoever.
Notice the lines in the function definitions that say
VISITOR_CATCH_MISSING_HEADER(cls)
. The reason for these lines, is that if you add a new class to the hierarchy, but forget to add an include to VisitedClasses.hpp, most compilers give you a confusing error message about initializing the variable p
. On the versions of g++ and Solaris CC I've looked at, the message says nothing about an undefined class, let alone a missing header file. So, in Visitor.cpp we define a macro which will give you an error message that points in the direction of the problem, if you have forgotten that include. The macro is:
#ifndef NDEBUG
#define VISITOR_CATCH_MISSING_HEADER(cls) \
typedef cls INCLUDE_ ## cls ## \
_hpp_IN_VisitedClasses_hpp; \
struct Dummy : \
public INCLUDE_ ## cls ## \
_hpp_IN_VisitedClasses_hpp { \
Dummy(); \
}
#else
#define VISITOR_CATCH_MISSING_HEADER(cls) /* nothing */
#endif
We make the macro a no-op for non-debug builds, though it shouldn't really affect the code even if you enable it for release builds. Compare the messages in g++ 4.1.2. Suppose you omitted
#include "IfElse.hpp"
from VisitedClasses.hpp. Without the macro, the message says VisitorSpecs.hpp:38: error: invalid initialization of reference of type 'const MultiStatement&' from expression of type 'const IfElse'; with the macro, you still get that message, but before it you get: VisitorSpecs.hpp:38: error: invalid use of undefined type 'struct Visitor::enter(const IfElse&)::INCLUDE_IfElse_hpp_IN_VisitedClasses_hpp'. A strange message, but it gets the point across better.Speaking of compiler errors, we will beef up the macro definitions in Visitor.hpp to catch a common coding error at compile time instead of having to debug strange behavior at runtime. The issue is this: since the
enter()/exit()
functions in our Visitor are not pure virtual, if you mistakenly add a concrete Visitor with one or more incorrect prototypes for those functions, the code will compile, link, and run -- silently skipping the functions with the incorrect prototypes, because the abstract base Visitor implementations will be found in the virtual table.For example, if your concrete Visitor declared
bool enter(const MultiStatement *)
-- with a pointer argument instead of a reference -- then you wouldn't know anything was wrong until you ran the program. In the worst case, you would not even notice that whatever behavior was supposed to happen in that MultiStatement enter()
didn't happen; even if you did notice it, you would have to step through in the debugger to watch the visit go into the wrong function. To preclude some of the more common silent-but-deadly typos, we bulk up the Visitor.hpp macros like so:
// The "real" prototypes.
#define VISITED_CLASS_ENTER_EXIT(cls,parent) \
virtual bool enter(const cls &); \
virtual void exit(const cls &);
// Fakes that turn typos into compile errors.
#define FORBID_PTR_ARG(cls,parent) \
virtual _USE_REFERENCE_ARG *enter(const cls *) \
{ return 0; } \
virtual _USE_REFERENCE_ARG *exit(const cls *) \
{ return 0; }
#define FORBID_NON_CONST_ARG(cls,parent) \
virtual _USE_CONST_REF *enter(cls &) \
{ return 0; } \
virtual _USE_CONST_REF *exit(cls &) \
{ return 0; }
#define FORBID_CONST_FUNC(cls,parent) \
virtual _USE_NON_CONST_FUNC *enter(const cls &) \
const { return 0; } \
virtual _USE_NON_CONST_FUNC *exit(const cls &) \
const { return 0; }
// Final macro definitions.
#ifdef NDEBUG
#define VISITED_CLASS(cls,parent) \
VISITED_CLASS_ENTER_EXIT(cls,parent)
#else
class _USE_REFERENCE_ARG;
class _USE_CONST_REF;
class _USE_NON_CONST_FUNC;
#define VISITED_CLASS(cls,parent) \
VISITED_CLASS_ENTER_EXIT(cls,parent) \
FORBID_PTR_ARG(cls,parent) \
FORBID_NON_CONST_ARG(cls,parent) \
FORBID_CONST_FUNC(cls,parent)
#endif
#define VISITED_ABSTRACT_CLASS(cls, parent) \
VISITED_CLASS(cls,parent)
#include "VisitorSpecs.hpp"
Without the
FORBID*
macros, the incorrect pointer-arg prototype above would prevent our FindLhsVars visitor in our previous example from finding assigned-to variables in complex statements such as blocks or if-else statements. With the macros in place, we get the following compile-time error: VisitorSpecs.hpp:36: error: overriding 'virtual Visitor::_USE_REFERENCE_ARG* Visitor::enter(const MultiStatement*)'. That is money in the bank.
Are you still with me? Then there's one more feature of this setup that I'd like to point out. Notice that
VISITED_CLASS
and its related macros can be defined in any file that includes VisitorSpecs.hpp. As useful as Default Visitor is, there are some concrete Visitors that must be updated whenever a new class is added to the hierarchy -- for example, the pretty-printer Visitor from the earlier post. In that case, instead of hard-coding the enter()
function declarations in the concrete Visitor class definition, do this:
#define VISITED_CLASS(cls,parent) \
virtual bool enter(const cls &);
#include "VisitorSpecs.hpp"
Now, if you forget to define the enter function for a newly-added class, you will get a link error when the linker tries to build the virtual table. From g++ 4.1.2: Print.cpp:61: undefined reference to `Print::enter(IfElse const&)'. Again, money in the bank. You might also find that there is some boilerplate code you would like to generate for every visited class -- a thought that comes to mind is an enum representing class types in the hierarchy. Simply extend the macro to include a third argument for that extra information -- if it can't be created with a '##' concatenation -- and include VisitorSpecs.hpp in the appropriate place.
Whew! This is a huge post, but when you apply these macro definitions to the improved Visitor in the earlier post, you have a very powerful idiom to work with. I can see a couple more posts coming up in this series: 1. a listing of the final Statement, Expression, and Visitor code presented here (I've been compiling it as I wrote these posts, to make sure it all worked), and an example
main()
to exercise it; and 2. discussion of a few Visitor issues I've glossed over to get this thing out.
No comments:
Post a Comment