Using Types to Avoid Cross-Cutting

June 26, 2010

Consider the following code.

void Log( char const *const s )
{
#ifdef DEBUG
    printf( "%s\n", s );
#endif
}

Doesn’t look too bad, eh?  I mean, nobody really likes to rely on the preprocessor but you gotta do what you gotta do, right?  I see two problems: firstly, it mixes cross-cutting concerns, namely logging and conditional execution; secondly, it conditionally compiles.  The former problem makes the code harder to read and maintain whilst the second facilitates “it compiles on MY machine” syndrome.

Rather than sprinkling #ifdefs through code, consider defining “twin” classes, one containing the conditional code and the other being a “null” class containing *NO* code whatsoever.  Then use the #ifdef to define which type to use. The client code is then clean i.e. no #ifdefs, release builds should elide the null class completely, and both the conditional code AND the null class are always compiled.

struct LoggerNull
{
    static void Insert( char const* )
    {
    }
};

struct LoggerImpl: public LoggerNull
{
    static void Insert( char const* const s )
    {
        printf( "%s\n", s );
    }
};

#ifdef DEBUG
    typedef LoggerImpl Logger;
#else
    typedef LoggerNull Logger;
#endif

void Log( char const* const s )
{
    Logger::Insert( s );
}

Does this code look cleaner to you?  Overwrought?  Let’s step through it.

LoggerNull provides the client interface and an empty implementation.  LoggerImpl derives from LoggerNull so that it only need hide those methods that it needs to.  Note that LoggerImpl does not override LoggerNull’s methods.  It hides them.  There is no vtable involved here and that is intentional: the intent of the original Log() method was to have zero overhead in release builds.  Note also that there is no guarantee at the language level that LoggerImpl is actually hiding a LoggerNull method.  Function signatures must match precisely.  There is no way to explicitly tell C++ of the relationship we desire between LoggerNull and LoggerImpl.  Finally, look at how apparent the responsibilities of LoggerNull and LoggerImpl are: there are no cross-cutting concerns present.

The #ifdef DEBUG shows up when we’re selecting our Logger type.  It deals with a single concern, that being which type to use as Logger in different build configurations.  It does not control compilation of those types.

The final implementation of Log() is syntactic sugar.

Now, consider what happens when LoggerNull and LoggerImpl get out of sync.  By this I mean that one type implements a method that the other does not.  As soon as you flip the configuration switch, you get a compile error.  Best time in the world to catch an error, right?  Then the coder who causes it to look closely at his new Logger method and how it’s used.  He can decide whether the new functionality is debug-only or is necessary for release.  It forces him to address that question before he can check in. “It compiles on MY machine” syndrome can still happen, but it should be far less frequent since both types are always compiled.

This toy example may be insufficient to demonstrate all the benefits to this technique.  When I produce some hobby code that uses this technique in earnest, I’ll post it.

About these ads

2 Responses to “Using Types to Avoid Cross-Cutting”

  1. Marilyn Says:

    And all this time I’ve never considered the cross-cutting. Thanks for letting me see the light.

  2. Chris Ozeroff Says:

    I like this method, this would reduce a lot of the #ifdefs in the codebase I am currently working on.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: