(April 2012)
struct FatPointAmbient { /* The screen-space X coordinate */ float _x; /* The camera space Z coordinate */ float _z; /* The triangle color scaled by the ambient occlusion factor */ Pixel _color; ... inline operator+=( const FatPointAmbient& rhs) { _x += rhs._x; _z += rhs._z; _color += rhs._color; } inline operator-=( const FatPointAmbient& rhs) { _x -= rhs._x; _z -= rhs._x; _color -= rhs._color; } inline operator*=(const float& rhs) { _x *= rhs; _z *= rhs; _color *= rhs; } inline operator/=(const float& rhs) { _x /= rhs; _z /= rhs; _color /= rhs; } }
This is a struct
that is used to interpolate various values per-pixel, as the renderer draws a scanline from left to right.
It's fields must therefore be incremented / decremented / multiplied / divided by the corresponding operators.
Never mind the specifics of the logic, though - do you consider this code as adhering to "not repeating oneself"?
Also, did you notice that in the operator-=
we have a typo? We are decrementing _z
by rhs._x
, instead of rhs._z
. It's not only more effort to maintain repetitive patterns - copy/paste also introduces errors, which will only be discovered at runtime (and force you to pay the price of debugging to fix them).
The renderer is in fact using 5 such types:
...and each one of them has its own fields - e.g. FatPointPhongAndSoftShadowed has...
/* The screen-space X coordinate */ float _projx; /* The camera space coordinates */ float _x,_y,_z; /* The (hopefully available from shadevis) ambient occlusion factor */ float _ambientOcclusionCoeff); /* The normal vector will also be interpolated (in camera space, */ /* so it is transformed in the FillerPhong) */ Vector3 _normal;
We could use inheritance to share the common fields, but that would do nothing about the repetition going on in the operators. Imagine my Fillers.h
, the file that defines these struct
s. It must be repeating information all over, right?
Well... no.
It is clear that the 4 operators are sharing many similar parts - so we can do something like this:
struct FatPointAmbient { /* The screen-space X coordinate */ float _x; /* The camera space Z coordinate */ float _z; /* The triangle color scaled by the ambient occlusion factor */ Pixel _color; ... #define OPERATOR(T,action) \ inline operator action ## = (const T& rhs) { \ _x action ## = rhs._x; \ _z action ## = rhs._z; \ _color action ## = rhs._color; \ } OPERATOR(FatPointAmbient, +) OPERATOR(FatPointAmbient, -) ... }
The token-pasting preprocessor operator ##
is used to join '+' and '=' and form '+=' ; ditto for -=
, *=
, /=
.
This is already improving things a lot - but notice that there is still semantic repetition: the list of structure fields drives both the field declarations, and the work to be done inside each operator. If, for example, we find out that we must add another member field, we need to update both the list of declarations and the list of actions (in the OPERATOR
macro).
We also have to write two macros per FatPoint type, because the *=
and /=
operators take a float
, not a FatPoint...
.
Is it possible to remove this duplication of effort?
It is. The idea behind X-Macros is very simple: if you have a list of items that influence many parts of your code, you put that list inside a macro. In our case, the list of structure fields is the base of everything, so we declare a macro with them:
#define X_MEMBERS \ /* The screen-space X coordinate */ \ X(float,_projx) \ /* The camera space Z coordinate */ \ X(float,_z) \ /* The triangle color scaled by the ambient occlusion factor */ \ X(Pixel,_color)
The list must carry all the information we will need per element, taking under consideration all the places in the code that the list influences. In our case, we store the typespec and the fieldname.
The X_MEMBERS
macro, is itself invoking another macro, X
, providing it with all the info per list entry. The X
macro then does something with this information - it can e.g. emit our field declarations, by simply:
struct FatPointAmbient { // Member declarations #define X(typespec,fieldname) typespec fieldname; X_MEMBERS #undef X ...
When you first see this, it may confuse you a bit - but it's really quite simple: The X
macro is temporarily set to emit 'typespec fieldname;'
lines, and when X_MEMBERS
is invoked, output like this is generated:
struct FatPointAmbient { float _projx; float _z; Pixel _color; ...
In other words, you can consider X_MEMBERS
as a code-generating "subroutine": it will invoke the X
macro for each of the list elements.
We apply the same technique for the operators - i.e. starting from the list of struct
fields, we emit operator code:
struct FatPointAmbient { // Member declarations #define X(typespec,fieldname) typespec fieldname; X_MEMBERS #undef X // Operator += declaration OPERATOR(FatPointAmbient,+,FatPointAmbient) ...
The OPERATOR
macro invokes X_MEMBERS
, performing the work we need per each field:
#define OPERATOR(T1,action,T2) \ inline T1& operator action ## = (const T2& rhs) { \ X_MEMBERS ; \ return *this; \ }
This macro can in fact be seen as an evolution of the simple version shown in the previous section:
inline FatPointAmbient& operator+=(const FatPointAmbient& rhs) {
#define ACT1(fieldname, action) fieldname action ## = rhs. fieldname; #define X(typespec,fieldname) ACT1(fieldname,+)
// // Ambient-Occlusion-Only // #define X_MEMBERS \ /* The screen-space X coordinate */ \ X(float,_projx) \ /* The camera space Z coordinate */ \ X(float,_z) \ /* The triangle color scaled by the ambient occlusion factor */ \ X(Pixel,_color)
struct FatPointAmbient { // Member declarations #define X(typespec,fieldname) typespec fieldname; X_MEMBERS #undef X // Operator declarations (i.e '+=' on all fields, '-=' on all fields, etc) #define X(typespec,fieldname) ACT1(fieldname,+) OPERATOR(FatPointAmbient,+,FatPointAmbient) #undef X #define X(typespec,fieldname) ACT1(fieldname,-) OPERATOR(FatPointAmbient,-,FatPointAmbient) #undef X #define X(typespec,fieldname) ACT2(fieldname,*) OPERATOR(FatPointAmbient,*,float) #undef X #define X(typespec,fieldname) ACT2(fieldname,/) OPERATOR(FatPointAmbient,/,float) #undef X };
We go one step further and eliminate the FatPointAmbient
typespec:
#define T FatPointAmbient struct T { // Member declarations #define X(typespec,fieldname) typespec fieldname; X_MEMBERS #undef X // Operator declarations (i.e '+=' on all fields, '-=' on all fields, etc) #define X(typespec,fieldname) ACT1(fieldname,+) OPERATOR(T,+,T) #undef X #define X(typespec,fieldname) ACT1(fieldname,-) OPERATOR(T,-,T) #undef X #define X(typespec,fieldname) ACT2(fieldname,*) OPERATOR(T,*,float) #undef X #define X(typespec,fieldname) ACT2(fieldname,/) OPERATOR(T,/,float) #undef X };
...and we now have a concise, re-usable "FatPointGENERIC" declaration section, which we use for all our FatPoint types - we just provide the list of member fields in X_MEMBERS
, and #define T FatPoint...
.
The DRY principle is a very important part of coding. It can be a challenge, though - more so in some languages than others.
In C and C++, X-macros can help. The end result is consice and significantly reduces repetition (and the associated copy/paste errors). It can, however, seem "cryptic" the first time you see it - so if you use them, add a comment in your code that points to this blog post :‑)
Index | CV | Updated: Sat Oct 8 11:41:25 2022 |
The comments on this website require the use of JavaScript. Perhaps your browser isn't JavaScript capable; or the script is not being run for another reason. If you're interested in reading the comments or leaving a comment behind please try again with a different browser or from a different connection.