(April 2012)

Don't Repeat Yourself (via X-Macros)

Rendered statue
My renderer avoids code repetition
via C++ templates and 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 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 structs. It must be repeating information all over, right?

Well... no.

First attempt: a couple of simple macros

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?

X-Macros

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) {
* It then proceeds to generate the operator body - it can do this, because it has access to all the field info, via the `X_MEMBERS` X-macro. `X` is set like this:
#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 `FatPointAmbient` type declaration including all 4 operators, is this:
//
// 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....

Conclusion

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 :‑)



profile for ttsiodras at Stack Overflow, Q&A for professional and enthusiast programmers
GitHub member ttsiodras
 
Index
 
 
CV
 
 
Updated: Sat Oct 8 11:41:25 2022