Why is it considered "bad practice" to use macros in C++?

This article originally appeared here on Quora

It is a common practice to use macros, you just have to be aware of the proper use and unfortunate misuses of them, to avoid pitfalls. It is not “bad practice” as the question suggests. C++ as a language has been striving to eliminate them, but the language engineers have failed and failed again. You need macros.

Even later languages like C# have eliminated them, and yet a debate continues as to whether or not they should added back in. In fact, they are in there[1]. They attempted to replace macros with “snippets[2]” — so the old school “macro” appears to perform enough of a utility to warrant the feature’s redesign in later attempts to write “the ultimate programming language” . . . which is <ahem> not C#.

For every answer to this question I’ve read, there is a completely opposite result. I’m going to debunk these, but before I do, this is my take on macros:

  1. Macros are a distinct feature, they are not functions, though they look similar and can be confusing to inexperienced developers.
  2. They are useful, sometimes. They don’t solve every problem.
  3. They save time, sometimes. They can be difficult to debug unless they result in only syntax errors.
  4. C++ programmers irrationally shy away from them, don’t be a sheep — be a shepherd — it’s a tool, not a religion — use it well and it will serve you well.
  5. You have to parenthetically pad all parameters, always, unless there is a reason you shouldn’t.
  6. Yes, they collide and don’t respect namespaces.
  7. Yes, they can screw up your code if you fudge them.
  8. Yes, they can repeat bad code everywhere, but sometimes help you save the day simply by fixing the code in a single place (inside the macro)
  9. They don’t do everything. They won’t let you macro-ize the naming of symbols. Sub-symbols just don’t work. Sadly, here is a macro that doesn’t fly, but if it did, would provide additional utility:
#define DECLARE_TWO_CLASSES(x,y)   class xOne {}; class yTwo {};

"You should  never use a macro in any situation where a function is the intention.
... The key is that none of these things were doable with functions alone. Macros should never take the place of a function. They are a great tool, but should only be used when you need to do things that only they can do."

— Adam Helps, Software Engineer at Autodesk

Consider:

#if defined(DEBUG)
#define OUTPUT(x)  { OutputDebugStringSafe(x); }
#else
#define OUTPUT(x)  { cout << (x); }
#endif

This is perfectly valid. It will work. It reroutes a standard OUTPUT(x) call to an appropriate function endpoint. So, case in point, here is an example used of a macro that simplifies multiple function endpoints. Sure, there is no reason to do this ever:

#define OUTPUT(x)    myfunction(x)

Unless of course you want to later change myfunction to something else, then this macro helps you!

"Inline functions have similar speed properties to macros, but they don't have the disadvantages."

— Adam Helps, Software Engineer at Autodesk

Au contraire mon frere, inline functions are an entirely different beast than macros. A macro is replaced at compile time before compilation, whereas an inline function (all class methods by default are these) may or may not be different to a regular old function declaration, depending on the compiler[3].

This sentiment was repeated almost verbatim, by another answerer, Gary Zhang of NVIDIA[4]:

"Macros are unsafe, have no type checking and should be avoided whenever possible. An alternative to macro is inline function for some use cases."

He is right that a macro doesn’t respect a namespace. That’s just a caveat of using one. It also creates gangly long and confusing names, and sometimes colliding / conflicting macro names when merging code, but I cannot really think of a time that merging code is not without its own issues and quirks. Search / replace works fine for replacing macro names, as long as you are careful.

"Most development environments only show the raw source code in the debugger or when viewing compiler errors; you rarely see the source code after preprocessing. This makes both syntax errors and run-time errors in macros very difficult to understand, because you can't see the code the computer is actually working with."

— Adam Helps, Software Engineer at Autodesk

Some do. Microsoft Visual C++ intellisense linting sometimes gives me odd output when using macros. I agree, not perfect — but it’s not the macro’s fault, it’s the IDE’s fault!

"The parameters to a macro are inserted as raw text."

Yes, sort of — not exactly, they have to be distinct and complete symbols or block scopes — that’s why macro parameters should always be parenthetically padded (except in the rare instances when that is not possible), as is the recommendation from many programmers including Bjarne Stroustrup. I did this up above in my first example.

Adam goes on to admit he has used macros and encountered them — so it looks like avoiding them as a general rule is impossible as many, many libraries and code snippets inevitably use them. Understanding them is probably your best course of action. Macros are a facet of the compilation process, and a part of probably every C++ codebase. (#define DEBUG seems pretty ubiquitous, for example)


Andrew Phillips chimes in that he feels Adam’s assessment is “excellent” and goes on to give a moderately useful example, but then makes this boldly inaccurate statement:

"Note, you can't just throw curly braces around the if test inside the macro (create a new blockscope for the contents of the Macro), because the Macro, to look correct from a programming perspective should have a trailing semicolon in the regular code." 

— Andrew Philips, Programming for decades.

You actually can throw curly braces around the if test inside the macro if you want. I think he just had a brainfart here, probably because both the macro and the answerer are colluding two distinct issues. The assertion Andrew makes is nonetheless incorrect. You can create a new block scope inside a macro, but it’s relevant to the code you’ve invoked the macro in, not within the macro itself like code in a function would be. Also, it is important to note that nothing requires you to end a macro with a semicolon “from a programming perspective” or any other perspective. It just isn’t required unless the underlying language after macro expansion requires it to be syntactically correct.

Andrew’s example:

#define MY_ASSERT(t) if (!(t)) printf("fail\n")
function goEatSandwich(int a, int b) { cout << "puke" << a << b; }
int main() {
int a=622;
if (a>500) MY_ASSERT(b==6);
else goEatSandwich(a,b);
return 0;
}

Basically what he is getting at is that the MY_ASSERT macro has no dangling semicolon (;), but the truth is you can put a block inside the macro like this, which solves his problem:

#define MY_ASSERT_fix_one(t) { if (!t) printf("fail\n"); }

Likewise, you can wrap the original MY_ASSERT with a block when invoked, like this:

int main() {
 int a=633;
 if ( a>500 ) { MY_ASSERT(a); }
 else ...
}

However, in this example, he’s speaking to the fact that MY_ASSERT is indeed not being called like a function would be of the same name, and yes, he is right — which is why this comes up in the first place. What would have happened in his original example would resemble this:

int main() {
 if (a>500) if (!(t)) printf("fail\n"); else goEatSandwich(a,b);
}

Essentially, this means that the else will go to the second “interior” conditional if-statement, but this is actually a common problem and I recommend wrapping all interior if-statements, always, no matter if you use macros or not, to avoid writing code that creates confusing logic that doesn’t work as expected. It’s easy to miss (sometimes programmers referred to this as code that “hides” bugs from you, but what it really is is a semantic conundrum easily confusing the human who is proofreading the code, though the resulting code is valid it is most likely a logical mistake). So, again, not the macro’s fault — it’s the programmer’s fault for not using more block scoping.

This comes down to ambiguity and the collusion of English with C++ (two entirely different languages, with some related reserved words!) — to write code effectively, don’t use if-statements like that, ever, when using macros or not.

Better method:

int main() {
 int a=5,b=10;
 if ( a > 100 ) {
  if ( !(a && b) ) { ... } else { ... }
 } else {
  ...
 }
}

In the above corrected form you can clearly see the structure of the conditionals and the respective else clauses. There is no semantic ambiguity.


"Using macros can reduce the readability of your code. When you use them, you're basically creating a set of nonstandard language features."
—  Duncan Smith, Programmer by day (and night)

Regarding the claim that this creates a set of “nonstandard language features” — firstly that is incorrect, because Macros and Variadic Macros[5] are a feature of the language, just like classes, functions and templates are. You are using the C++ language if you are using its macro feature, period. (Purists will argue that it is technically outside the language specification.[6]) What you are actually doing is creating a set of custom macros that, unfortunately, cannot be shielded in a namespace, but are valid, and don’t change the underlying language in any way — in fact, the content they imply is completely subject to the rules of the language.

Employing macros, when used properly, can actually increase the readability of your code. Take this as an example, how C++ can be enhanced through careful use of smartly-designed macros: ZeroTypes Library for Visual C++

Consider the amount of productivity lost if you have to type, for 1800+ classes typical of a commercial desktop application, the following:

class MyClass : public BaseClassListItem {
public:
};
 
class MyClassCollective : public BaseClassLinkedList {
public:
 void Clear() {
  MyClassCollection *n=nullptr;
  for ( MyClassCollective *a=first; a; a=n ) {
   n=a->next;
   delete a;
  }
 }
};

Now consider the macro version from ZeroTypes[7]:

ONE(MyClass,{})
MANY(MyClass,MyClassHandle,MyClassHandles,MyClassCollective,{})
DONE(MyClass);
/*
 The above 3 lines declares both children as well as 2 other class types
 how useful!  Saves me hours of each day, as long as I am willing to
 follow this framework.  It's equivalent to the other code, but beefed
 up with commonly needed features for each distinct classtype I declare.
 */

I’ve seen lots of these “Class declaration macros” around in different libraries and codebases. ZeroTypes is definitely not the only set, but it strives to be a really useful set.

"The person reading your code (which may be you in the future after you have forgotten what your macros mean) has to expand the macros in their head as they're reading them."

Since when is it a good programming practice to not read and understand code? Perhaps the point of Duncan saying this is because he is complaining that it makes it harder. Sure - the macro content may not be right there, and yeah - you may need to think about it a bit and what it might mean in your head — and yes there are rabbit holes with macros, I must be clear — but it can be understood and it can be deciphered. So, it’s not like macros are truly hiding anything. Some compilers/IDE[8] even will show you code after it has been preprocessed, to see exactly what is happening after macro expansion has occurred.

I feel this comment, which I’ve heard and read before, comes from “irrational programmer fear of unknown code” and “laziness or fatigue from a long day’s work” — not practical or proactive behaviors. You often must read other people’s code to understand it. It’s also good practice to understand what underlying code is doing, by reading a library’s source (when available) or at least inspecting the data and reverse engineering “closed source” libraries.

You should always be proactive. You should be reading and understanding all of the implications of your code. We aren’t all savants, but if a human could write it, another equally-intelligent human could understand it, even if it is “macro-ized” by design.

"As  Gary Williams mentioned, using macros can increase the chance of bugs in your code if you're not very careful about expansion rules. And you may get less support from your tools."

Duncan is right about this — partly — except that macros don’t introduce bugs unless you introduce them by handling macro authoring improperly. Macros repeat poorly constructed code all over the place — so you really need to know what you are doing with your macros and test them thoroughly before deploying them everywhere, and you need to avoid jumping down a rabbit hole if you want to go and update them later. There’s a good chance your code won’t even compile.

The bottom line is: you have to be careful with your use of macros. Ask yourself: do you walk the razor’s edge, or do you succumb to drudgery out of fear and personal inadequacies? Or, do you rise to the challenge?

Use of macros can be a liberating experience, when done properly. As programmers we always have to work against bad design patterns and, in this case, your macros have to be carefully sculpted. Don’t let your own inadequacies of using a macro make you shy away from using them — simply retrain yourself to use them effectively, like any other feature of a language.

Personally, I think C++ engineers are embarrassed that something so insanely simple, invented way back at the beginning of the language (in fact, in its predecessor) could ultimately prove to be so powerful when wielded properly.

If you don’t know how to write Assembler effectively, you’re going to need to train yourself further, right? Why not the same for this powerful compilation tool that can save you tons of time, provide valuable help for cross-platform development, and make your code look more sane?

Footnotes

[1] Macros in C#

[2] Code Snippets

[3] Inline function - Wikipedia

[4] nv-tegra.nvidia.com Git - linux-2.6.git/commit

[5] The C Preprocessor: Variadic Macros

[6] Standard Predefined Macros

[7] h3rb/ZeroTypes

[8] Seeing expanded C macros

 

Why is it considered "bad practice" to use macros in C++?: https://www.linkedin.com/pulse/why-considered-bad-practice-use-macros-c-herbert-elwood-gilliland-iii

posted on 2024-05-26 00:23  yusisc  阅读(3)  评论(0编辑  收藏  举报

导航