The C ++ language opens up vast possibilities for dispensing with macros. So let's try to use macros as rarely as possible!
Immediately make a reservation that I am not a fanatic and do not call for abandoning macros for idealistic reasons. For example, when it comes to manually generating the same type of code, I can recognize the benefits of macros and put up with them. For example, I calmly refer to macros in old programs written using MFC. It makes no sense to fight something like this:
BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT )
There are such macros, yes and good. They really were created to simplify programming.
')
I’m talking about other macros with which they try to avoid implementing a full function or try to reduce the size of the function. Consider a few reasons to avoid such macros.
Note. This text was written as a guest post for the Simplify C ++ blog. Russian version of the article decided to publish here. Actually, I am writing this note in order to avoid a question from inattentive readers why the article is not marked as “translation” :). But, actually, the guest post in English: " Macro Evil in C ++ Code ".First: code with macros attracts bugs
I do not know how to explain the reasons for this phenomenon from a philosophical point of view, but this is so. Moreover, the bugs associated with macros are often very difficult to notice when conducting a code review.
I have repeatedly described such cases in my articles. For example,
replacing the isspace function with this macro:
#define isspace(c) ((c)==' ' || (c) == '\t')
The programmer who used
isspace believed that he was using the real function, which considers not only spaces and tabs as whitespace characters, but also LF, CR, etc. As a result, it turns out that one of the conditions is always true and the code does not work as expected. This error from the Midnight Commander is described
here .
Or how do you see such an abbreviation of writing the function
std :: printf ?
#define sprintf std::printf
I think the reader realizes that this was a very unfortunate macro. It was found, by the way, in the StarEngine project. Read more about this
here .
It can be argued that programmers are to blame for these errors, not macros. This is true. Naturally, programmers are always guilty of mistakes :).
It is important that macros provoke errors. It turns out that macros should be used with increased accuracy or not at all.
I can give examples of defects associated with the use of macros for a long time, and this nice note will turn into a weighty multi-page document. Of course, I will not do this, but I will show a couple of other cases for persuasiveness.
The ATL library
provides macros such as A2W, T2W and so on for converting strings. However, few people know that these macros are very dangerous to use inside loops. Inside the macro, a call is made to the
alloca function, which at each iteration of the loop will again and again allocate memory on the stack. The program can pretend that it works correctly. As soon as the program starts processing long lines or the number of iterations in the loop increases, the stack can take and end at the most unexpected moment. You can read more about this in this
mini-book (see the chapter “Do not call the alloca () function inside loops”).
Macros, such as A2W, hide evil. They look like functions, but, in fact, have side effects that are difficult to notice.
I can not go past such attempts to reduce the code using macros:
void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... }
Only the first line of the macro refers to the
if statement . The remaining lines will be executed regardless of the condition. We can say that this error is from the world of C, since it was found by me using the
V640 diagnostics inside the GCC compiler. GCC code is written mainly in C, and in this language it is hard to do without macros. But agree that this is not the case. Here it was quite possible to make a real function.
Second: the reading of the code is complicated
If you are faced with a project that is full of macros consisting of other macros, then you understand what the hell it is - to understand a similar project. If you have not encountered, then believe in the word, it is sad. As an example of hard-to-read code, I can cite the already mentioned GCC compiler.
According to legend, Apple has invested in the development of the LLVM project as an alternative to GCC due to the GCC code being too complex because of these very macros. Where I read about it, I do not remember, so there will be no proofs.
Third: writing macros is difficult
Easy to write bad macros. I meet them everywhere with appropriate consequences. But to write a good and reliable macro is often more difficult than writing a similar function.
It is difficult to write a good macro for the reason that, unlike a function, it cannot be considered as an independent entity. It is required to immediately consider the macro in the context of all possible uses, otherwise it is very easy to get the problem of the type:
#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]);
Of course, for such cases, bypass tricks have been invented long ago and the macro can be implemented safely:
#define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; })
Just a question, do we need all this in C ++? No, in C ++ there are templates and other ways to build efficient code. So why do I continue to meet similar macros in C ++ programs?
Fourth: debugging is complicated
It is believed that debugging is for weaklings :). This, of course, is interesting to discuss, but from a practical point of view, debugging is useful and helps to find errors. Macros complicate this process and definitely slow down the search for errors.
Fifth: false positives of static analyzers
Due to the specifics of their device, very many macros generate multiple false positives of static code analyzers. I can safely say that most of the false positives when checking C and C ++ code are associated with macros.
The trouble with macros is that analyzers simply cannot distinguish correct tricky code from erroneous code. In the
article about checking Chromium one of these macros is described.
What to do?
Let's not use macros in C ++ programs unless absolutely necessary!
C ++ provides rich tools, such as template functions, automatic type inference (auto, decltype), constexpr functions.
Almost always, instead of a macro, you can write an ordinary function. Often this is not done because of ordinary laziness. This laziness is harmful, and we must fight it. A little extra time spent writing a full feature will pay off with interest. The code will be easier to read and maintain. The chance to shoot yourself a leg will decrease, and compilers and static analyzers will produce fewer false positives.
Some may argue that code with a function is less efficient. This is also only an “excuse”.
Compilers are now perfectly inline code, even if you have not written the keyword
inline .
If we are talking about the calculation of expressions at the compilation stage, then here the macros are not needed and even harmful. For the same purposes,
constexpr is much better and safer.
Let me explain by example. This is a classic bug in the macro that I
borrowed from FreeBSD Kernel code.
#define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE))
The
chan argument is used in a macro without wrapping parentheses. As a result, the expression
IC-2400_VPOPT_WRITE_SIZE is not multiplied by the expression
(chan - 1) , but only one.
An error would not occur if instead of a macro an ordinary function was written.
size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
With high probability, modern C and C ++ compilers will independently perform
inlining functions, and the code will be as effective as in the case of macros.
In this case, the code has become more readable, as well as free from errors.
If it is known that the input value is always a constant, then you can add
constexpr and be sure that all calculations will occur at the compilation stage. Imagine that this is a C ++ language and that
chan is always a kind of constant. Then the function
ICB2400_VPINFO_PORT_OFF is useful to declare as:
constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
Profit!
I hope I managed to convince you. Good luck and fewer macros in the code!