Preprocessor pitfalls
The key to the problems of preprocessor
macros is that you can be fooled into thinking that the behavior of the
preprocessor is the same as the behavior of the compiler. Of course, it was
intended that a macro look and act like a function call, so it’s quite
easy to fall into this fiction. The difficulties begin when the subtle
differences appear.
As a simple example, consider the
following:
#define F (x) (x + 1)
Now, if a call is made to F like
this
F(1)
the preprocessor expands it, somewhat
unexpectedly, to the following:
(x) (x + 1)(1)
The problem occurs because of the gap
between F and its opening parenthesis in the macro definition. When this
gap is removed, you can actually call the macro with the
gap
F (1)
and it will still expand properly
to
(1 + 1)
The example above is fairly trivial and
the problem will make itself evident right away. The real difficulties occur
when using expressions as arguments in macro calls.
There are two problems. The first is that
expressions may expand inside the macro so that their evaluation precedence is
different from what you expect. For example,
#define FLOOR(x,b) x>=b?0:1
Now, if expressions are used for the
arguments
if(FLOOR(a&0x0f,0x07)) // ...
if(a&0x0f>=0x07?0:1)
The precedence of & is lower
than that of >=, so the macro evaluation will surprise you. Once you
discover the problem, you can solve it by putting parentheses around everything
in the macro definition. (This is a good practice to use when creating
preprocessor macros.) Thus,
#define FLOOR(x,b) ((x)>=(b)?0:1)
Discovering the problem may be difficult,
however, and you may not find it until after you’ve taken the proper macro
behavior for granted. In the un-parenthesized version of the preceding macro,
most expressions will work correctly because the precedence of
>= is lower than most of the operators like +, /, –
–, and even the bitwise shift operators. So you can easily begin to
think that it works with all expressions, including those using bitwise logical
operators.
The preceding problem can be solved with
careful programming practice: parenthesize everything in a macro. However, the
second difficulty is subtler. Unlike a normal function, every time you use an
argument in a macro, that
argument is evaluated. As long as the macro is called only with ordinary
variables, this evaluation is benign, but if the evaluation of an argument has
side effects, then the results can be surprising and will definitely not mimic
function behavior.
For example, this macro determines
whether its argument falls within a certain range:
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
As long as you use an
“ordinary” argument, the macro works very much like a real function.
But as soon as you relax and start believing it is a real function, the
problems start. Thus:
//: C09:MacroSideEffects.cpp
#include "../require.h"
#include <fstream>
using namespace std;
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
int main() {
ofstream out("macro.out");
assure(out, "macro.out");
for(int i = 4; i < 11; i++) {
int a = i;
out << "a = " << a << endl << '\t';
out << "BAND(++a)=" << BAND(++a) << endl;
out << "\t a = " << a << endl;
}
} ///:~
Notice the use of all upper-case
characters in the name of the macro. This is a helpful practice because it tells
the reader this is a macro and not a function, so if there are problems, it acts
as a little reminder.
Here’s the output produced by the
program, which is not at all what you would have expected from a true
function:
a = 4
BAND(++a)=0
a = 5
a = 5
BAND(++a)=8
a = 8
a = 6
BAND(++a)=9
a = 9
a = 7
BAND(++a)=10
a = 10
a = 8
BAND(++a)=0
a = 10
a = 9
BAND(++a)=0
a = 11
a = 10
BAND(++a)=0
a = 12
When a is four, only the first
part of the conditional occurs, so the expression is evaluated only once, and
the side effect of the macro call is that a becomes five, which is what
you would expect from a normal function call in the same situation. However,
when the number is within the band, both conditionals are tested, which results
in two increments. The result is produced by evaluating the argument again,
which results in a third increment. Once the number gets out of the band, both
conditionals are still tested so you get two increments. The side effects are
different, depending on the argument.
This is clearly not the kind of behavior
you want from a macro that looks like a function call. In this case, the obvious
solution is to make it a true function, which of course adds the extra overhead
and may reduce efficiency if you call that function a lot. Unfortunately, the
problem may not always be so obvious, and you can unknowingly get a library that
contains functions and macros mixed together, so a problem like this can hide
some very difficult-to-find bugs. For example, the
putc( ) macro in cstdio may evaluate
its second argument twice. This is specified in Standard C. Also, careless
implementations of toupper( ) as a macro may
evaluate the argument more than once, which will give you unexpected results
with
toupper(*p++).[45]