Refactoring: Guard Macro Body

You have a C/C++ macro that needs to perform several statements or contains conditional statements.

Wrap the statements in a do { } while (0) construct to ensure that the statements are executed in the proper order and that the use of the macro doesn’t interfere with other control structures in the program.

Motivation

Historicly, the C language has not had support for inline functions. To compensate for this, small code fragments that were intended to be reused were often implemented as preprocessor macros.

If the body of the macro expands to a single expression, then the macro can be used as a single-statement block with control flow statements:

if (test)
    MACRO();
else
    OTHER_MACRO();

However, should either of the macro definitions MACRO or OTHER_MACRO expand to multiple statements and separate those statements with a semi-colon, then we have a problem:

#define MACRO()        expr1; expr2
#define OTHER_MACRO()  expr3; expr4

The expanded code becomes:

if (test)
    expr1; expr2;
else
    expr3; expr4;

You can insulate yourself by enclosing macro invocations in braces:

if (test)
{
    MACRO();
}
else
{
    OTHER_MACRO();
}

One way around this problem is to use the comma operator to sequence a series of expressions. Some find this use of the comma operator confusing. The main drawback of the comma operator for this problem is that it can’t handle the use of control statements within the macro itself.

The canonical way to wrap a multi-statement macro is to place the entire body of the macro inside a do { } while (0) statement. Unlike the previous approaches, dowhile is a valid single statement and it also provides a local scope where macro arguments can be evaluated once and assigned to temporaries in order to avoid unexpected side-effects with macro arguments.

Of course its much better to replace a macro with some other mechanism that is type-safe and doesn’t rely on textual substitution. That could be a template function, an inline function, or a method on a class. However, if you’re using C code and you can’t convert it to C++ quite yet, you can at least improve the robustness of the macro by making it work properly in control constructs without requiring an additional set of curly braces.

Mechanics

  1. Write a test to ensure that the new macro definition can be used in control statements without additional curly braces.
  2. Compile the test and see that it fails.
  3. Reformat the multi-statement macro’s definition to include a single statement per-line, with each statement terminated by a semi-colon (;) and the line ending in a backslash (\) to continue the macro definition across multiple lines.
  4. Insert the text do { at the start of the macro definition’s body. Insert the text } while (0) at the end of the macro definition’s body. Note the lack of a terminating semi-colon after the while’s conditional clause.
  5. Recompile the test and see that it succeeds. If it doesn’t compile, debug the macro and the test until it compiles successfully.
  6. Compile any source files that are dependent upon the macro definition. Correct any compile errors that result.

Example

This example is taken from FractInt, an open source program for rendering fractal images. The file fractals.cpp contains a number of macros for use with testing the bailout condition of an iterated fractal formula. BAIL_OUT_TRIG_FP() is one such macro:

#define BAIL_OUT_TRIG_FP() \
    if (fabs(g_old_z.y) >= g_rq_limit2) return 1

1. Write a failing test for the macro

We can write a failing test for this macro that embeds it within an if statement where the macro definition would change the control flow.

int x = 0;
bool success = false;
if (x > 0)
    BAIL_OUT_TRIG_FP();
else
    success = true;
assert(success);

2. Compile the test and see that it fails.

When this test is run, the assertion will fail because of the way the macro expands:

int x = 0;
bool success = false;
if (x > 0)
    if (fabs(g_old_z.y) >= g_rq_limit2)
        return 1;
    else
        success = true;
assert(success);

3. Reformat the macro

Normalizing the body of the macro to look like normal inline code gives the following:

#define BAIL_OUT_TRIG_FP()              \
    if (fabs(g_old_z.y) >= g_rq_limit2) \
        return 1

4. Insert the guard text

While inserting the guard text around the macro body, we can also enclose the then clause of the if statement in curly braces.

#define BAIL_OUT_TRIG_FP()                      \
    do                                          \
    {                                           \
        if (fabs(g_old_z.y) >= g_rq_limit2)     \
        {                                       \
            return 1;                           \
        }                                       \
    }                                           \
    while (0)

5. Recompile and run the test

The test will now pass because the scope of the if statement in the macro’s body is localized.

6. Compile any dependent source files

Your build system will handle this step; you may find that localizing the effect of macro bodies causes some syntax errors in files using these macros. These are most likely errors that have been hiding in your code due to the structure of the macro. If they are instances where the behavior of the macro’s expansion was purposefully exploited, then the code will need to be modified to work correctly again.

2 Responses to “Refactoring: Guard Macro Body”

  1. jirko Says:

    Why are you using do…while? Simply using curly braces should be enough.

    Like

  2. legalize Says:

    Because the macro is invoked like it is a statement, the user is expected to terminate the macro invocation with a semi-colon. Notice the lack of a semi-colon on the do/while statement. If you simply use curly braces, then the following will not compile:

    if (test) MACRO(); else OTHER_MACRO();
    

    Like


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 )

Google+ photo

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

Connecting to %s

%d bloggers like this: