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:
Now,
if a call is made to
f
like this
the
preprocessor expands
it, somewhat unexpectedly, to the following:
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
and
it will still expand properly, to
The
above example 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))
// ...
The
precedence of
&
is lower than that of
>=,
so the macro evaluation will surprise you. Once you discover the problem (and
as a general practice when creating preprocessor macros) you can solve it by
putting parentheses around everything in the macro definition. 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
unparenthesized version of the preceding example,
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. The second difficulty is more subtle. 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:Macro.cpp
// Side effects with macros
#include <fstream>
#include "../require.h"
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;
}
} ///:~
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
stdio.h
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++).[26]
Macros
and access
Of
course, careful coding and use of preprocessor macros are required with C, and
we could certainly get away with the same thing in C++ if it weren’t for
one problem: A macro has no concept of the scoping required
with member functions. The preprocessor simply
performs text substitution, so you cannot say something like
class X {
int i;
public:
#define val (X::i) // Error
or
anything even close. In addition, there would be no indication of which object
you were referring to. There is simply no way to express class scope in a
macro. Without some alternative to preprocessor macros, programmers will be
tempted to make some data members
public
for the sake of efficiency, thus exposing the underlying implementation and
preventing changes in that implementation.
[26]Andrew
Koenig goes into more detail in his book
C
Traps & Pitfalls
(Addison-Wesley, 1989).
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru