|
Unix Programming - Designing Minilanguages - Macros — Beware!
Macros — Beware!
Macro expansion facilities were a favored tactic for language
designers in early Unix; the C
language has one, of course, and we have seen them show up in some of
the more complex special-purpose minilanguages like
pic(1).
The m4 preprocessor provides a generic tool for implementing
macro-expanding preprocessors.
Macro expansion is easy to specify and implement, and you can do
a lot of cute tricks with it. Those early designers appear to have been
influenced by experience with assemblers, in which macro facilities
were often the only device available for structuring programs.
The strength of macro expansion is that it knows nothing about
the underlying syntax of the base language, and can be used to extend
that syntax. Unfortunately, this power is very easily abused to
produce code that is opaque, surprising, and a fertile source of
hard-to-characterize bugs.
In C, the classic example of this sort of problem is a macro
such as this:
#define max(x, y) x > y ? x : y
There are at least two problems with this macro. One is that it
can produce surprising results if either of the arguments is an
expression including an operator of lower precedence than > or ?:.
Consider the expression max(a = b,
++c). If the programmer has forgotten that max is a macro, he will be expecting the
assignment a = b and the preincrement
operation on c to be executed before
the resulting values are passed as arguments to max.
But that's not what will happen. Instead, the preprocessor will
expand this expression to a = b > ++c ? a = b :
++c, which the C compiler's precedence rules make it
interpret as a = (b > ++c ? a = b :
++c). The effect will be to assign to a!
This sort of bad interaction can be headed off by coding the
macro definition more defensively.
#define max(x, y) ((x) > (y) ? (x) : (y))
With this definition, the expansion would be ((a = b) > (++c) ? (a = b) : (++c)). This
solves one problem — but notice that c may be incremented twice! There are subtler
versions of this trap, such as passing the macro a function-call with
side effects.
In general, interactions between macros and expressions with
side effects can lead to unfortunate results that are hard to
diagnose. C's macro processor is a deliberately lightweight and
simple one; more powerful ones can actually get you in worse
trouble.
The TeX formatting
language (see Chapter18) well illustrates the general
problem. TeX is intentionally
Turing-complete (it has conditionals, loops, and recursion), but while
it can be made to do amazing things, TeX code tends to be unreadable and
painful to debug. The sources for LaTeX, the the most widely used TeX macro package, are an instructive
example: they're in very good TeX style, but even so are extremely
difficult to follow.
A minor problem, compared to this one, is that macro expansion
tends to screw up error diagnostics. The base language processor
generates its error reports relative to the macro expanded text, not
the original the programmer is looking at. If the relationship
between the two has been obfuscated by macro expansion, the emitted
diagnostic can be very difficult to associate with the actual
location of the error.
This is especially a problem with preprocessors and macros
that can have multiline expansions, conditionally include or exclude
text, or otherwise change line numbers in the expanded text.
Macro expansion stages that are built into a language can do
their own compensation, fiddling line numbers to refer back to the
preexpanded text. The macro facility in
pic(1)
does this, for example. This problem is more difficult to solve when
the macro expansion is done by a preprocessor.
The C preprocessor
addresses this problem by emitting #line directives whenever it does an inclusion
or multiline expansion. The C compiler is expected to interpret these
and adjust the line numbers in its error reports accordingly.
Unfortunately, m4 has no such
facility.
These are reasons to use macro expansion with extreme caution.
One of the long-term lessons of the Unix experience is that macros
tend to create more problems than they solve. Modern language and
minilanguage designs have moved away from them.
[an error occurred while processing this directive]
|