- 2.1. Our emphasis
- 2.2. The basic goal—a major difference between C++ and Java
- 2.3. Constructors and destructor
- 2.4. Operator overloading in C++
- 2.5. Operator overloading in Java
- 2.6. Flow-control constructs
- 2.7. Manipulating character strings in C++
- 2.8. Canonical class structure
- 2.9. Overcoming macrophobia
- 2.10. Program readability
- 2.11. Error detection and exceptions
2.9. Overcoming macrophobia
2.9.1. Bad macros
C++ insiders disparage the use of the preprocessor, inherited from C. Java goes further by not supporting a preprocessor in the standard language. Experts, including the designers of both languages, advise against using macros. Stroustrup warns:
- “The first rule about macros is: Don’t use them if you don’t have to. Almost every macro demonstrates a flaw in the programming language, in the program, or in the programmer.10
What really offends Stroustrup and other experts are these two common uses of macros in traditional C:
defining constants:
#defineNITEMS 32
implementing generic pseudo-functions:
#defineABS(x)((x) >= 0 ? x :-(x))
Both of those macro definitions are handled more flexibly and more reliably by features of C++:
const int NITEMS = 32; template<class T> inline T abs(const T x) {return x >= 0 ? x : -x;}
or Java:
public static final int NITEMS = 32; public T abs(final T x)//Member of class T {return x.greaterThan(0) ? x : x.minus();}
2.9.2. Good macros
The main benefit of macros in any programming language is that they provide a way of capturing and packaging patterns of code. Specifically, macros are able to
- localize such patterns for ease of future change
- facilitate compliance with standards for such patterns
- minimize or eliminate opportunities for error
- enhance source-code readability
There is no other way in C++ (and no way at all in Java) to capture code patterns that are not functions. Here’s a small, simple example:
#define ACCESSOR(name, rtnType, expr) rtntyp name() {return expr;}
A programmer defining the Complex class could use that macro in the class definition:
ACCESSOR(realPart, double, rp) ACCESSOR(imagPart, double, ip) ACCESSOR(rho, double, sqrt(rp*rp + ip*ip)) ACCESSOR(theta, Angle, atan2(ip/rp))
Since maintenance programmers scanning the source code would immediately spot those functions and understand their purpose, you wouldn’t need any explanatory commentary. But don’t worry—we’re not going to use that sort of low-level macro coding in this book.
2.9.3. Packaging common patterns in elementary numeric classes
Defining a robust, production-quality class can be awfully tedious and error-prone. We need to implement dozens of methods, follow dozens of rules, and avoid dozens of pitfalls.
Preprocessor macros are the only C++ facility that can help. In Chapter 4, we package the additive pattern, and in Chapter 5, we package the point-extent pattern using mainly the simplest of all preprocessor facilities, the #include macro statement. In Chapter 6, we use some ad hoc macros to reduce the repetition of smaller patterns of code, simplifying future program maintenance.
Those simple examples may stir your imagination to consider this approach whenever you find yourself repeating the same pattern of code over and over.
Problems and exercises
2.9-1 Among standard higher-level languages, PL/I provides the most powerful macro (or preprocessor) language. Consult a PL/I manual to learn how macros work, and then try to find some examples that exploit those facilities. Discuss whether a similarly powerful preprocessor would help or hurt program development and maintenance in C++ or Java.
2.9-2 The switch case construct (in Section 2.6.2) works only for integer types. Using macro coding, devise an equivalent flow-control construct switch case that works for any data type. Document any reasonable restrictions due to limitations of the macro language (forbidding nested occurrences of this construct, for example).
2.9.4. #include dependencies
Many #include files, especially class definitions, depend on definitions in other #include files. There are two ways of handling such situations:
The usage documentation for file A can tell the user that file B has to be included first, like this:
#include B #include A
- File A can itself contain the preprocessor statement #include B. If file B contains definitions that the compiler should see only once, then it’s customary to surround the code with a multiple-include guard, usually #ifndef . . . . #endif.
The second technique has become common practice among C and C++ programmers, because it localizes knowledge and relieves the programmer of complicated bookkeeping. Nevertheless, there are a few situations in which the first technique is preferable.
One such situation is the packaging of truly global definitions, such as constants, functions, and macros that nearly every program needs and that the programmer should hardly need to think about. Such definitions are like extensions to the C++ language itself.
The online companion to this book, www.dorsethouse.com/books/ooc.html, contains a minimal set of such definitions, file global.hpp. Most organizations will want to expand it to include more conventional names and functions that they wish to be standard for their programming staff or at least for an individual project team—for example, an #include for a string class.
Some experts claim that use of macros impairs program readability by introducing unfamiliar syntax. Experience shows, however, that macros can greatly facilitate readability among groups of programmers who are acquainted with them and use them every day.
This technique also reduces compile times, since global.hpp is opened only once per compile. When programs grow very large, compile times can become surprisingly long; compilers may waste a lot of that time by opening #include files only to skip to the end and close the file because of the multiple-include guard.
Problems and exercises
2.9-3 C programmers often call #include files “header files.” Discuss how that tradition may have originated and whether it’s reasonable today to call every includable source-code file a “header.”
2.9-4 Java’s designers consider a general #include facility unnecessary and potentially dangerous. Consider this view and decide whether you concur. State persuasive reasons to support your position.
2.9-5 A few of the generic functions in global.hpp—such as min(x,y)—duplicate those available in the standard template library (STL). What considerations justify having them in global.hpp?
2.9.5. Macros in this book
In later chapters, we occasionally exploit macros when C++ provides no alternative facility:
- to capture patterns of code for reuse in multiple components.
- to “factor out” tedious repetition within a local component to enhance readability and avoid error. The global.hpp example above uses a couple of such local macros.
We refrain, however, from using macros as extensively in this chapter as we do in production code. That way, readers can skip this chapter and still be able to understand every example.