An Introductory Overview of C++ Macros
Macros have been a growing source of curiosity for me in the past year. I generally hadn’t given them much thought and just used them in the kinds of situations I’d been told to. But it seems like whenever they’re mentioned in documentation, online forums and presentations there’s a common theme. You can do cool things with macros, you shouldn’t do those cool things, and you probably don’t need to anyway. For some reason though nobody really says specifically what the cool things are, or why you shouldn’t do them. Maybe that’s just common knowledge to everyone else, but the general love-hate vibe surrounding the topic makes me wonder how something so seemingly simple and tiny elicits such strong feelings from people.
🔗 So what exactly is a macro?
A macro is a kind of preprocessor directive. They originated in C and were carried over into C++. There’s a handy C preprocessor manual mixed in with the GCC documentation that defines a macro very simply as “a fragment of code which has been given a name.” You can define your own macros using
#define followed by a name for the macro and then whatever code you want it to represent. During the preprocessing stage before compilation, every instance of that macro in your program will be replaced with the code it represents. It certainly seems simple enough.
Macros are often divided into two categories, object-like and function-like. These mean exactly what they sound like. Object-like macros associate a value with a name and are often used to improve readability and clarity. For example if you have some constant with special significance in your program or some piece of code that is particularly long and used often, you might define a macro to take its place:
#define PI 3.14159 #define RESOLUTION 1920 #define MAX_RESOLUTION RESOLUTION #define LOG_ERROR std::cout<<"Presto, have a default error message"<<std::endl;
Note from that third example that you can include macros within other macros, or even within themselves (and there is some protection against macros infinitely recursing on themselves). Function-like macros can be much more complicated, combining operations and optionally taking in arguments to behave in ways similar to functions.
#define SQUARE(x) ((x) * (x)) // SQUARE(3) -> 9 #define MIN(x, y) ((x) < (y) ? (x) : (y)) // MIN(2, 5) -> 2 #define PRINT_MIN(x,y) std::cout<< "Min of " << \ x << " and " << y << " is " << \ MIN(x, y) <<std::endl // PRINT_MIN(2, 5) -> Min of 2 and 5 is 2
There are also many predefined macros, which if you’re like me you may have been using without even realizing what they were. They generally begin with a double underscore, like
__date__. Some of them are standardized within the language and some are system-specific.
That’s just the tip of the iceberg of course, and you can make all sorts of crazy combinations with nested macros, conditional definitions,
#undef, and other features. But at the end of the day, replacing fragments of code with descriptive names that the preprocessor will just swap back for you at compile time doesn’t seem like it would be a problem. As Jens Weller said in his microtalk on modern C++ macros, “it’s just fancy text replacement.” So what’s the issue?
🔗 Why Not to Use Them
Human error. Ultimately this is what it seems to come down to. One drawback to all the freedom in macros is there are countless ways for things to quietly implode without breaking the rules, giving you unintended and incorrect results without ever complaining or throwing errors.
For example, ending a macro with a semicolon and then using it in the middle of an expression could end the expression prematurely, but leaving the semicolon out can also cause unexpected results if you’re not paying attention. Order of operations when passing even simple expressions into function-like macros can go haywire, and self-referencing can easily have unintended effects. Since macros don’t have namespaces it’s entirely possible to accidentally overwrite a preexisting macro. And if there is any sort of problem, it can be significantly harder to pinpoint as debuggers often can’t look into the values of object-like macros, and multi-line inputs to function-like macros can mess up line numbering in error messages.
A few examples from the GCC documentation of subtle but potentially damaging issues in macros are included below:
#define strange(file) fprintf (file, "%s %d", strange(stderr) p, 35); /* Unbalanced parentheses can be confusing The code above evaluates to be syntactically correct: fprintf (stderr, "%s %d", p, 35) This can be okay if intentional, but very bad if accidental */ #define min(X, Y) ((X) < (Y) ? (X) : (Y)) next = min (x + y, foo (z)); /* Evaluates to next = ((x + y) < (foo (z)) ? (x + y) : (foo (z))); foo could be applied twice, changing the underlying data or increasing runtime */ #define ceil_div(x, y) (x + y - 1) / y a = ceil_div (b, c + d); /* Evaluates to a = (b + c + d - 1) / c + d; Order of operations is unclear at first glance User may assume c+d is evaluated before division, but this is not the case */
Then why use macros? If it’s so easy to mess them up, and ultimately they’re just more or less object-like and function-like substitutions, why not just use standard objects and functions? And unless you’re working with legacy code bases, there are less and less reasons to use macros as C++ continues to expand. In his Cppcon talk on becoming macro-free, Zhihao Yuan noted that features like lambdas, inline functions, and constexpr have covered many of the traditional uses for macros, and some common predefined macros like NULL and TYPEOF have also been replaced with equivalent standard alternatives. He did point out that in the areas of include guards, logging, metadata, and unit testing macros still aren’t obsolete, but that more techniques to replace them are either already available or on the horizon.
Macros open up a world of random and interesting possibilities in C++. They give programmers freedom to bend the rules of normal C++ syntax and structure in creative ways. At the same time it’s up for debate whether or not they should even be used, and the general concensus seems to be that they’re best avoided unless absolutely necessary. Still they’re a pretty fascinating carryover from C that’s worth looking into further and experimenting with.
🔗 Comments? Questions?
Jump on the twitter thread below to chat!! ☎️ ☎️ ☎️