Les macros en C++ : beauté dangereuse ou mal nécessaire ?
\n\nQuiconque a déjà écrit en C++ a rencontré les macros. D'un côté, c'est un outil puissant pour la métaprogrammation qui peut réduire la quantité de code passe-partout. De l'autre, les macros sont réputées pour leur caractère perfide. Il suffit d'oublier un point-virgule ou de mal placer des parenthèses pour que le code se transforme en un monstre imprévisible.
\n\nMais il existe dans le monde du C++ une catégorie particulière de macros qui semble franchement étrange. Avez-vous déjà vu ce code ?
\n\n#define SAFE_DELETE(ptr) do { delete (ptr); (ptr) = nullptr; } while(0)\n\nC'est logique : supprimer le pointeur et le mettre à null. Mais pourquoi cette boucle do-while(0) apparemment inutile ? Une boucle qui s'exécute forcément une seule fois ? Cela ressemble à une redondance frôlant l'absurde. Ou encore cet exemple avec if-else :
#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)\n\nTout semble simple. Mais c'est ici que se cachent les pièges dans lesquels des milliers de développeurs sont tombés. Cette question sur Stack Overflow a recueilli des dizaines de milliers de vues, et aujourd'hui nous allons voir pourquoi ces constructions ne sont pas un bug mais une fonctionnalité, et quelles erreurs de macros C++ elles évitent.
\n\nPourquoi les macros ne sont pas des fonctions ? Le problème principal
\n\nAvant de plonger dans les méandres du do-while(0), rappelons ce qui distingue une macro d'une fonction classique. En C++, une macro est une substitution textuelle. Le préprocesseur copie simplement le corps de la macro à l'endroit où elle est appelée. Aucune vérification de type, aucune portée — une simple substitution de chaînes.
C'est précisément pour cela que surviennent les erreurs classiques dans les macros C++. Prenons un exemple simple :
\n\n#define SQUARE(x) x * x\n\nint main() {\n int a = 5;\n int result = SQUARE(a + 1); // On attend 36, on obtient 11\n std::cout << result; // Affiche 11\n return 0;\n}\n\nQue s'est-il passé ? La macro s'est développée en a + 1 * a + 1. À cause de la priorité des opérateurs, la multiplication a été effectuée avant l'addition. Première erreur : absence de parenthèses. Corrigeons :
#define SQUARE(x) ((x) * (x))\n\nMaintenant, ça fonctionne. Mais ce n'est que le début. Les vrais problèmes commencent lorsque la macro contient plusieurs instructions ou des structures de contrôle.
\n\nL'idiome do-while(0) : un bouclier contre les catastrophes syntaxiques
\n\nRevenons à notre SAFE_DELETE. Pourquoi ne pas écrire la macro plus simplement, sans cette boucle étrange ?
#define SAFE_DELETE(ptr) delete (ptr); (ptr) = nullptr;\n\nCela semble logique. Mais imaginez que vous l'utilisiez dans ce contexte :
\n\nif (ptr != nullptr)\n SAFE_DELETE(ptr);\nelse\n std::cout << "Pointer is null";\n\nAprès substitution, le préprocesseur transforme cela en :
\n\nif (ptr != nullptr)\n delete (ptr);\n (ptr) = nullptr;\nelse\n std::cout << "Pointer is null";\n\nEt voilà le bug ! Le else n'est plus rattaché au if car une deuxième instruction est apparue entre eux. Le compilateur générera une erreur, mais l'idée est claire : la macro a « cassé » la structure de contrôle. C'est exactement pour ces cas que do-while(0) est nécessaire. Il transforme plusieurs instructions en une seule instruction composée, préservant la logique :
if (ptr != nullptr)\n do { delete (ptr); (ptr) = nullptr; } while(0);\nelse\n std::cout << "Pointer is null";\n\nMaintenant, tout fonctionne correctement. L'idiome des macros do-while(0) est une technique standard qui garantit que la macro se comporte comme une instruction unique.
\n\nPourquoi spécifiquement do-while(0) et pas simplement un bloc { } ?
\n\nBonne question ! On pourrait simplement utiliser des accolades :
\n\n#define SAFE_DELETE(ptr) { delete (ptr); (ptr) = nullptr; }\n\nEssayons d'utiliser