Macro magic or Macro hell?Filed Under: Weekly Tuesday Dose of goodness
Hi all,
This week’s article is delayed due to my hectic schedule. I have to run around and therefore will have very little time to post my latest article.
Many of us use preprocessors such as #define, #ifdef, #else and many others. Most of the time when people are first introduced to C programming, one of the things they’ll learn is - always use #define to define your constants.
As we enter into more complicated stages of programming, we’ll usually find other preprocessors such as #ifdef, #ifndef and #else.
In C++, the use of #define is extremely undesirable, falling short of making it forbidden. So, is C++’s standards too paranoid or is it justified? Let’s find out…
Before I begin the article proper, let me clarify that this article is based mostly on conditions using preprocessors and is not an article to explain every preprocessor functionality.
Next, macro magic or macro hell?
This question is based on the preprocessing stage (which some might not know, but it’s not a problem) that is executed right before the code is being compiled.
As we know, all the #defines are resolved into their constants just before compilation. This is known as the preprocessing stage where the source files are expanded into a .i (immediate) file where all the dependencies are dumped into this single file.
Why is there a need for the preprocessing stage? The reason is because, code paths are selected during this phase before compilation can take place. You may think, what code paths? Aren’t they already fixed in the codes using if-else, switch-cases? Well, yes, but that’s not all to it!
Macro Magic
Here’s a very typical example of how codes are selected:
void println(const char* string)
{
#ifdef CPLUS_PLUS
cout << string << "\n" << endl;
#else
printf("%s\n", string);
#endif
}
Here, you can see that if the application is built to run with C++, then cout will be used always. Otherwise, printf is used. This sorts of have an “Object-Oriented” flavour in it, but the difference is, this is done during compile time, OO can be achieve the same effect during runtime.
A good advantage of this example is that, your entire application can use a single call to print text onto the screen. The implementation can change but only at the critical section and not across the project.
Therefore to make the C++ code effective, you’ll have to either,
1) Define #define CPLUS_PLUS somewhere in a header that is accessed by the println() function.
2) In the build options, preprocessors, include CPLUS_PLUS or by command line, -DCPLUS_PLUS.
-D is used for most compilers, however for Visual Studio, it should be /D instead.
What if you don’t define CPLUS_PLUS? Your application will use printf instead due to the #else block.
Let’s take a look at another example:
#ifdef WIN32
#include <windows.h>
#endif
#ifdef LINUX
#include <linux.h> //for example
#endif
void sleep(unsigned long milliseconds)
{
#ifdef WIN32
Sleep(milliseconds); //using Win32 API
#endif
#ifdef LINUX
sleep(milliseconds); // I'm not sure if linux sleeps this way, please correct me
#endif
}
So, you’ll need -DWIN32 in order to call the Win32 API. This is done automatically (the magicial part) due to the macro. During preprocessing, the following codes will exists in the immediate file should -DWIN32 be defined.
#include <windows.h>
void sleep(unsigned long milliseconds)
{
Sleep(milliseconds);
}
This piece of code will only work in a WIN32 environment. If -DWIN32 is defined in a Linux environment, do your maths and you’ll know that it’ll definitely fail compilation.
Now, at this point, it’s still obvious and easy to fix such problems since it’s still just 1 or 2 macros.
Macro Hell
This occurs as a result of dependencies in between the preprocessors conditions. For example, if MY_API is defined, then the following must be defined:
1) MY_UTIL
2) MY_CORE
3) MY_DATATYPE
Then if MY_UTIL is selected, the following macros must be in the build as well:
1) MY_UTIL_THREADS
2) MY_UTIL_ORCHESTRATION
So, this is just the beginning because it’s just but one of the MANY MANY combinations that exists in the build. Welcome to the Macro Hell!
So it ends up having tons of -D options in your build command line. For example:
g++ -g -DWIN32 -DMY_API -DMY_UTIL -DMY_UTIL_ORCHESTRATION mySrc.cpp -c -o mySrc.o
In fact, having these many -Ds are not the main problem. The crux lies in the dependencies between each macro. If the wrong set of codes are chosen and compiled, then you’ll either get VERY weird compilation errors that are almost impossible to resolve without the build master, or you’ll get a very erratic application behavior.
Figure that out? Nah, I’d rather redo my entire build configuration. At least that’ll give me a clean start.
Macro Functions or Macro Mass Confusion?
I’m sure many of us have at least 1 or more Macro functions written to help us perform certain tasks without having to type a lot of codes.
I’ve also seen quite a few people that have nested macro function calls. Guess what? In each macro function, there’re several condition branches. Each branch will lead to another macro call. Yeah, you get the drift?
This make it insanely hard to debug as well as to measure. Some tools that help measure McCabe Cyclomatic Complexity will report errenous-looking but correct measurements of the code as well. (ie, the MCC index becomes REALLY high, ie > 30 for a function with 5 lines of codes).
For example:
#define MACRO_INNER_FUNC(a) (a>2?-5:5) #define MACRO_INNER_FUNC2(a) (a?a*2:a*5) #define MACRO_FUNC(b) (b>10?MACRO_INNER_FUNC(b):MACRO_INNER_FUNC2(b)) #define MACRO_DEFUNC(b) (b<5?MACRO_INNER_FUNC(MACRO_INNER_FUNC2(b)):MACRO_INNER_FUNC2(MACRO_INNER_FUNC(b)))
int checkCondition(int a, int b, int c)
{
if(a < 0 || MACRO_FUNC(a) > 2)
return -1;
if(MACRO_FUNC(b) > 1 && MACRO_DEFUNC(c) < 0) return -1;
return MACRO_FUNC(a) + MACRO_DEFUNC(b) + MACRO_DEFUNC(c); }
Work that out and you’ll know what I mean!
The MCC for this function would be : 35!!!
* That means this simple 5 liner function has 35 condition points! (MCC always start with 1 by the way)
Conclusion
This is really a classic example of:
If you can’t convince them (that you’re a super coder), confuse them (by using nested macro functions)!
To be honest, I’ve been setting up projects all around the region for people who are using my company’s testing tools. I’ve much less afraid of missing header files as compared to weird compilation errors. The last straw comes when the customer says this extremely popular statement:
“oh, I don’t know! My build works, why not yours?”
Well, though I can’t answer that question directly, but this would be my direct answer:
“Oh well, I’m not YOUR build master and I’m not an Oracle as well! Do you think I can understand how your 1,000,000 lines of code project works JUST with a glance?!!!”
Some people simply expect me or my company’s tool to be able to understand their functional logic. That silver bullet seeker mentality is really driving me nuts.
Anyway, we’re getting out of point here. But that’s all I have regarding Macros. In the next article next week, I’ll talk about the Silver Bullet Seeker mentality that so many people possess.
Regards,
Jeremy
- Permalink
- Admin
- 28 Oct 2009 6:19 PM
- Comments (0)