Dommages macro pour le code C ++

définir

Le langage C ++ offre de vastes possibilités pour se passer de macros. Essayons donc d'utiliser le moins possible les macros!

Faites immédiatement une réserve que je ne suis pas un fanatique et n'encouragez pas à abandonner les macros pour des raisons idéalistes. Par exemple, lorsqu'il s'agit de générer manuellement le même type de code, je peux reconnaître les avantages des macros et les accepter. Par exemple, je suis calme sur les macros dans les anciens programmes écrits en utilisant MFC. Cela n'a aucun sens de se battre avec quelque chose comme ça:

BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT ) //{{AFX_MSG_MAP(efcDialog) ON_WM_CREATE() ON_WM_DESTROY() //}}AFX_MSG_MAP END_MESSAGE_MAP() 

Il y a de telles macros, et d'accord. Ils ont vraiment été créés pour simplifier la programmation.

Je parle d'autres macros avec lesquelles ils essaient d'éviter la mise en œuvre d'une fonction à part entière ou essaient de réduire la taille d'une fonction. Considérez plusieurs motifs pour éviter de telles macros.

Remarque Ce texte a été écrit en tant qu'invité pour le blog Simplify C ++. J'ai décidé de publier la version russe de l'article ici. En fait, j'écris cette note afin d'éviter une question de lecteurs inattentifs pourquoi l'article n'est pas marqué comme «traduction» :). Et ici, en fait, un article invité en anglais: " Macro Evil in C ++ Code ".

Premièrement: le code macro attire les bogues


Je ne sais pas comment expliquer les raisons de ce phénomène d'un point de vue philosophique, mais ça l'est. De plus, les bogues liés aux macros sont souvent très difficiles à repérer lors d'une révision de code.

J'ai décrit à plusieurs reprises de tels cas dans mes articles. Par exemple, en remplaçant la fonction isspace par cette macro:

 #define isspace(c) ((c)==' ' || (c) == '\t') 

Le programmeur qui a utilisé isspace pensait qu'il utilisait une vraie fonction qui considère non seulement les espaces et les tabulations comme des espaces, mais aussi LF, CR, etc. Le résultat est que l'une des conditions est toujours vraie et que le code ne fonctionne pas comme prévu. Cette erreur de Midnight Commander est décrite ici .

Ou comment aimez-vous ce raccourci pour écrire la fonction std :: printf ?

 #define sprintf std::printf 

Je pense que le lecteur devine que c'était une macro très infructueuse. Il a d'ailleurs été trouvé dans le projet StarEngine. En savoir plus à ce sujet ici .

On pourrait dire que les programmeurs sont à blâmer pour ces erreurs, pas les macros. C'est vrai. Naturellement, les programmeurs sont toujours à blâmer pour les erreurs :).

Il est important que les macros provoquent des erreurs. Il s'avère que les macros doivent être utilisées avec une précision accrue ou pas du tout.

Je peux donner des exemples de défauts associés à l'utilisation de macros depuis longtemps, et cette belle note se transformera en un document de plusieurs pages. Bien sûr, je ne le ferai pas, mais je vais montrer quelques autres cas de persuasion.

La bibliothèque ATL fournit des macros telles que A2W, T2W, etc. pour convertir des chaînes. Cependant, peu de gens savent que ces macros sont très dangereuses à utiliser à l'intérieur des boucles. À l'intérieur de la macro, la fonction alloca est appelée, qui allouera de la mémoire sur la pile à chaque itération de la boucle. Un programme peut prétendre fonctionner correctement. Dès que le programme commence à traiter de longues lignes ou que le nombre d'itérations dans la boucle augmente, la pile peut prendre et se terminer au moment le plus inattendu. Vous pouvez en savoir plus à ce sujet dans ce mini-livre (voir le chapitre «N'appelez pas la fonction alloca () dans les boucles»).

Des macros comme A2W cachent le mal. Ils ressemblent à des fonctions, mais ont en fait des effets secondaires difficiles à remarquer.

Je n'arrive pas à surmonter les tentatives similaires de réduction de code à l'aide de 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) .... } 

Seule la première ligne de la macro fait référence à l' instruction if . Les lignes restantes seront exécutées quelle que soit la condition. Nous pouvons dire que cette erreur vient du monde C, car elle a été trouvée par moi en utilisant les diagnostics V640 dans le compilateur GCC. Le code GCC est écrit principalement en C, et dans ce langage les macros sont difficiles à faire. Cependant, vous devez admettre que ce n'est pas le cas. Ici, il était tout à fait possible de faire une vraie fonction.

Deuxièmement: la lecture de code devient plus compliquée


Si vous tombez sur un projet plein de macros composées d'autres macros, alors vous comprenez ce que c'est que de comprendre un tel projet. Si vous ne l'avez pas rencontré, alors prenez un mot, c'est triste. Comme exemple de code difficile à lire, je peux citer le compilateur GCC mentionné précédemment.

Selon la légende, Apple a investi dans le développement du projet LLVM comme une alternative à GCC en raison de la complexité du code GCC en raison de ces mêmes macros. Là où je l'ai lu, je ne m'en souviens pas, donc il n'y aura pas de preuves.

Troisièmement: écrire des macros est difficile


Il est facile d'écrire une mauvaise macro. Je les rencontre partout avec des conséquences correspondantes. Mais écrire une macro bonne et fiable est souvent plus difficile que d'écrire une fonction similaire.

Écrire une bonne macro est difficile car, contrairement à une fonction, elle ne peut pas être considérée comme une entité indépendante. Il est nécessaire de considérer immédiatement la macro dans le contexte de toutes les options possibles pour son utilisation, sinon il est très facile de ratisser un problème de forme:

 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]); 

Bien sûr, pour de tels cas, les solutions de contournement ont longtemps été inventées et la macro peut être implémentée en toute sécurité:

 #define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; }) 

La seule question est, avons-nous besoin de tout cela en C ++? Non, en C ++, il existe des modèles et d'autres façons de créer du code efficace. Alors, pourquoi dois-je continuer à rencontrer des macros similaires dans les programmes C ++?

Quatrièmement: le débogage est compliqué


Il y a une opinion que le débogage est pour les mauviettes :). Ceci, bien sûr, est intéressant à discuter, mais d'un point de vue pratique, le débogage est utile et aide à trouver des erreurs. Les macros compliquent ce processus et ralentissent définitivement la recherche d'erreurs.

Cinquièmement: les faux positifs des analyseurs statiques


De nombreuses macros, en raison des spécificités de leur appareil, génèrent plusieurs faux positifs à partir d'analyseurs de code statiques. Je peux dire en toute sécurité que la plupart des faux positifs lors de la vérification du code C et C ++ sont associés à des macros.

Le problème avec les macros est que les analyseurs ne peuvent tout simplement pas distinguer le code délicat correct du code erroné. L' article sur la vérification de Chromium décrit l'une de ces macros.

Que faire


N'utilisons pas de macros dans les programmes C ++ à moins que cela ne soit absolument nécessaire!

C ++ fournit des outils riches tels que les fonctions de modèle, l'inférence de type automatique (auto, decltype), les fonctions constexpr.

Presque toujours, au lieu d'une macro, vous pouvez écrire une fonction ordinaire. Souvent, cela n'est pas fait à cause d'une paresse ordinaire. Cette paresse est nuisible et nous devons la combattre. Un petit temps supplémentaire consacré à la rédaction d'une fonction à part entière sera payant avec intérêt. Le code sera plus facile à lire et à maintenir. La probabilité de tirer sur votre propre jambe diminuera et les compilateurs et les analyseurs statiques produiront moins de faux positifs.

Certains pourraient soutenir que le code avec une fonction est moins efficace. C'est aussi juste une "excuse".

Les compilateurs intègrent désormais parfaitement le code, même si vous n'avez pas écrit le mot clé inline .

Si nous parlons de calculer des expressions au stade de la compilation, alors les macros ne sont pas nécessaires et même nuisibles. Dans le même but, il est bien meilleur et plus sûr d'utiliser constexpr .

Je vais vous expliquer avec un exemple. Voici une erreur de macro classique que j'ai empruntée au code du noyau FreeBSD.

 #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)) // <= static void isp_fibre_init_2400(ispsoftc_t *isp) { .... if (ISP_CAP_VP0(isp)) off += ICB2400_VPINFO_PORT_OFF(chan); else off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <= .... } 

L'argument chan est utilisé dans une macro sans encapsuler entre parenthèses. Par conséquent, l'expression ICB2400_VPOPT_WRITE_SIZE ne multiplie pas l'expression (chan - 1) , mais une seule.

L'erreur n'apparaîtrait pas si une fonction ordinaire était écrite au lieu d'une macro.

 size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; } 

Il est très probable que le compilateur C et C ++ moderne exécute lui- même des fonctions d' inline , et le code sera aussi efficace que dans le cas d'une macro.

Dans le même temps, le code est devenu plus lisible et exempt d'erreurs.

S'il est connu que la valeur d'entrée est toujours une constante, vous pouvez ajouter constexpr et être sûr que tous les calculs auront lieu au stade de la compilation. Imaginez que c'est C ++ et que chan est toujours une constante. Ensuite, il est utile de déclarer la fonction ICB2400_VPINFO_PORT_OFF comme ceci:

 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!

J'espère avoir réussi à vous convaincre. Bonne chance et moins de macros dans le code!

Source: https://habr.com/ru/post/fr444612/


All Articles