Don't Repeat Yourself (via X-Macros) |
|
|||||||||
(April 2012) Don't Repeat Yourself (via X-Macros)
The principle of not repeating yourself in your code sounds nice, in theory. Why would anyone choose to maintain two (or more) identical parts instead of just one? Well, it turns out that sometimes it's really hard to avoid repeating yourself. The repetition is not 'identical' - it's slightly off. Take this code section from my renderer, for example: 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 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 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 Well... no. First attempt: a couple of simple macrosIt 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 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 We also have to write two macros per FatPoint type, because the Is it possible to remove this duplication of effort? X-MacrosIt 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 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 struct FatPointAmbient { float _projx; float _z; Pixel _color; ... In other words, you can consider We apply the same technique for the operators - i.e. starting from the list of struct FatPointAmbient { // Member declarations #define X(typespec,fieldname) typespec fieldname; X_MEMBERS #undef X // Operator += declaration OPERATOR(FatPointAmbient,+,FatPointAmbient) ... The #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,+) i.e. it ignores the typespec, and passes the fieldname and the actual op to ACT1, emiting the '+=' action for each field. So the end result for our // // 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 #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 ConclusionThe 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 :-)
|
|
|||||||||