Macros in C++: Dangerous Beauty or Necessary Evil?
\n\nAnyone who has ever written in C++ has encountered macros. On one hand, they are a powerful tool for metaprogramming that can reduce the amount of boilerplate code. On the other hand, macros are notorious for their insidiousness. Just miss one semicolon or place parentheses incorrectly, and the code turns into an unpredictable monster.
\n\nBut there is a special category of macros in the C++ world that looks frankly strange. Have you ever seen code like this?
\n\n#define SAFE_DELETE(ptr) do { delete (ptr); (ptr) = nullptr; } while(0)\n\nIt seems logical: delete the pointer and set it to null. But why is this meaningless do-while(0) here? A loop that is guaranteed to execute exactly once? It looks like redundancy bordering on absurdity. Or here's another example with if-else:
#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)\n\nIt seems simple. But this is exactly where the rake lies that thousands of developers have stepped on. This question on Stack Overflow has gathered tens of thousands of views, and today we will break down why such constructs are not a bug but a feature, and what C++ macro errors they prevent.
\n\nWhy Are Macros Not Functions? The Main Problem
\n\nBefore diving into the intricacies of do-while(0), let's recall how a macro differs from a regular function. In C++, a macro is a textual substitution. The preprocessor simply copies the macro body to the place where it is called. No type checks, no scopes — just a string replacement.
It is because of this that classic errors in C++ macros arise. Let's look at a simple example:
\n\n#define SQUARE(x) x * x\n\nint main() {\n int a = 5;\n int result = SQUARE(a + 1); // Expect 36, get 11\n std::cout << result; // Will output 11\n return 0;\n}\n\nWhat happened? The macro expanded to a + 1 * a + 1. Due to operator precedence, multiplication was performed before addition. The first error is the lack of parentheses. Let's fix it:
#define SQUARE(x) ((x) * (x))\n\nNow it works. But these are just the beginning. Real problems start when a macro contains multiple statements or control structures.
\n\nThe do-while(0) Idiom: A Shield Against Syntactic Disasters
\n\nLet's return to our SAFE_DELETE. Why not write the macro simpler, without this strange loop?
#define SAFE_DELETE(ptr) delete (ptr); (ptr) = nullptr;\n\nIt seems logical. But imagine using it in a context like this:
\n\nif (ptr != nullptr)\n SAFE_DELETE(ptr);\nelse\n std::cout << "Pointer is null";\n\nAfter substitution, the preprocessor will turn this into:
\n\nif (ptr != nullptr)\n delete (ptr);\n (ptr) = nullptr;\nelse\n std::cout << "Pointer is null";\n\nAnd there it is — the bug! The else no longer belongs to the if because a second statement appeared between them. The compiler will throw an error, but the point is clear: the macro "broke" the control structure. This is exactly the case where do-while(0) is needed. It turns multiple statements into one compound statement, preserving the logic:
if (ptr != nullptr)\n do { delete (ptr); (ptr) = nullptr; } while(0);\nelse\n std::cout << "Pointer is null";\n\nNow everything works correctly. The macro idiom do-while(0) is a standard technique that guarantees the macro will behave as a single statement.
\n\nWhy do-while(0) and Not Just a Block { }?
\n\nGood question! You could just use curly braces:
\n\n#define SAFE_DELETE(ptr) { delete (ptr); (ptr) = nullptr; }\n\nLet's try using