C / C++ macros are a pain in the ass sometimes…

I got burnt by a macro issue today and wanted to share it in case you are writing macros.

The goal was to write a macro to assist with making unit/system test checks explicit and when they failed, to print out some information to track back to the condition that failed.

This is on an embedded system so I don’t think I can use catch2, my go-to framework, I’m pretty sure it requires posix features not supported on Zephyr, the rtos I’m using.

In any case I had a need to write a macro that would work like:

/** @return 0 upon success, non-zero upon failure */
int test_function()
{
    int value = somefunction();
    REQUIRE(value == 3);

    return 0; // success
}

Where REQUIRE() would return the non-zero value if the condition wasn’t true.

My first attempt

So lets write the macro.

// Remember to always wrap macro parameters with () to ensure
// parameter evaluation occurs correctly
#define REQUIRE(condition) if(!(condition)) { printf("%s:%d failure\n", __FILE__, __LINE__); return -1; }

This macro sort-of works. It prints out the error string and returns. But what if we want a few variations, one that returns and one that doesn’t, like:

#define REQUIRE(condition) if(!(condition)) { printf("%s:%d failure\n", __FILE__, __LINE__); }
#define REQUIRE_RET(condition) REQUIRE(condition); if(!(condition)) { return -1; }

And if we look at our test function this works as expected:

#include <stdio.h>
#include <stdbool.h>

#define REQUIRE(condition) if(!(condition)) { printf("%s:%d failure\n", __FILE__, __LINE__); }
#define REQUIRE_RET(condition) REQUIRE(condition); if(!(condition)) { return -1; }

int main()
{
    REQUIRE_RET(true == false);

    return 0;
}
cmorgan@Chriss-MacBook-Pro test % cc macrotest.c
cmorgan@Chriss-MacBook-Pro test % ./a.out
macrotest.c:39 failure

This looks good, working as expected, but there is a hidden bug.

A more advanced test case could be:

#include <stdio.h>
#include <stdbool.h>

#define REQUIRE(condition) if(!(condition)) { printf("%s:%d failure\n", __FILE__, __LINE__); }
#define REQUIRE_RET(condition) REQUIRE(condition); if(!(condition)) { return -1; }

bool is_set;

/**
 * @return true if not yet set and false if already set
 */
bool set_if_not_set()
{
    if(!is_set)
    {
        is_set = true;
        return true;
    } else
    {
        return false;
    }
}

int main()
{
    // we expect this to be true since 'is_set' is initialized to zero
    // at program start
    REQUIRE_RET(set_if_not_set() == true);

    return 0;
}
cmorgan@Chriss-MacBook-Pro test % cc macrotest.c
cmorgan@Chriss-MacBook-Pro test % ./a.out
cmorgan@Chriss-MacBook-Pro test % echo $?
255

This is odd. We had no failure printout, yet the exit code (the return value from main) is clearly -1 (255).

What’s going on here?

The bug

The bug is in this macro:

#define REQUIRE_RET(condition) REQUIRE(condition); if(!(condition)) { return -1; }

Notice that (condition) appears multiple times.

This is totally fine if the condition is one without side effects, such as:

REQUIRE_RET(val < 100);

But as soon as the condition has side effects, like:

REQUIRE_RET(set_if_not_set() == true);

It’s apparent that the condition itself is duplicated twice in the macro and thus evaluated twice. We end up with

if(!(set_if_not_set() == true)) { printf(xxx); }; if(!(set_if_not_set() == true)) { return -1; }

And set_if_not_set() is being called twice…. booo

The solution

The solution is to evaluate the condition into a temporary variable once and enclose the entire macro in brackets so the temporary goes out of scope after the macro line is passed. Without this scoping there would be a multiple definition issue for the temporary variable.

The fixed macro:

#define REQUIRE_RET(condition) { bool val = (condition); REQUIRE(val); if(!(val)) { return -1; } }

It was a dumb oversight but took a few minutes to figure out as no errors were printed yet my test function was returning as if a test failed.

C/C++ macros are helpful but not always the most straightforward to use as you get to more complex use cases.

Updated: