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.