web-dev-qa-db-fra.com

Pires pratiques en C ++, erreurs courantes

Après avoir lu ce fameux coup de gueule de Linus Torvalds , je me suis demandé quels étaient en fait tous les pièges pour les programmeurs en C++. Je ne fais explicitement pas référence à des fautes de frappe ou à un flux de programme incorrect traité dans cette question et ses réponses , mais à des erreurs de plus haut niveau qui ne sont pas détectées par le compilateur et n'entraînent pas de bogues évidents à première exécution, erreurs de conception complètes, choses improbables en C mais susceptibles d'être effectuées en C++ par des nouveaux arrivants qui ne comprennent pas toutes les implications de leur code.

Je me réjouis également des réponses indiquant une baisse considérable des performances là où on ne s'y attendrait généralement pas. Un exemple de ce qu'un de mes professeurs m'a dit une fois à propos d'un générateur d'analyseur LR (1) que j'ai écrit:

Vous avez utilisé un peu trop d'instances d'héritage et de virtualité inutiles. L'héritage rend une conception beaucoup plus compliquée (et inefficace en raison du sous-système RTTI (inférence de type à l'exécution)), et elle ne doit donc être utilisée que lorsque cela a du sens, par ex. pour les actions de la table d'analyse. Parce que vous utilisez intensivement les modèles, vous n'avez pratiquement pas besoin d'héritage. "

35
Felix Dombek

Torvalds parle de son cul ici.


OK, pourquoi il parle de son cul:

Tout d'abord, sa diatribe n'est vraiment rien MAIS diatribe. Il y a très peu de contenu réel ici. La seule raison pour laquelle il est vraiment célèbre ou même légèrement respecté, c'est parce qu'il a été créé par le Dieu Linux. Son principal argument est que C++ est de la merde et il aime énerver les gens C++. Il n'y a bien sûr aucune raison de répondre à cela et quiconque le considère comme un argument raisonnable est de toute façon hors de conversation.

Quant à ce qui pourrait être considéré comme ses points les plus objectifs:

  • STL et Boost sont des conneries <- peu importe. Tu es un idiot.
  • STL et Boost provoquent des douleurs infinies <- ridicules. De toute évidence, il exagère exagérément, mais alors quelle est sa vraie déclaration ici? Je ne sais pas. Il y a des problèmes plus que trivialement difficiles à résoudre lorsque vous faites vomir le compilateur dans Spirit ou quelque chose, mais ce n'est pas plus ou moins difficile à comprendre que le débogage de UB causé par une mauvaise utilisation des constructions C comme void *.
  • Les modèles abstraits encouragés par C++ sont inefficaces. <- Comme quoi? Il ne se développe jamais, ne donne jamais d'exemples de ce qu'il veut dire, il le dit simplement. BFD. Puisque je ne peux pas dire à quoi il fait référence, il est inutile d'essayer de "réfuter" la déclaration. C'est un mantra commun des bigots C mais cela ne le rend pas plus compréhensible ou intelligible.
  • Une utilisation correcte de C++ signifie que vous vous limitez aux aspects C. <- En fait, le code WORSE C++ fait cela, donc je ne sais toujours pas WTF dont il parle.

Fondamentalement, Torvalds parle de son cul. Il n'y a aucun argument intelligible sur quoi que ce soit. S'attendre à une réfutation sérieuse de telles absurdités est tout simplement stupide. On me dit de "développer" une réfutation de quelque chose que je devrais développer si c'est moi qui l'ai dit. Si vous regardez vraiment, honnêtement, ce que Torvalds a dit, vous verriez qu'il n'a rien dit.

Ce n'est pas parce que Dieu dit que cela n'a aucun sens ou devrait être pris plus au sérieux que si un bozo aléatoire le disait. À vrai dire, Dieu n'est qu'un autre bozo aléatoire.


Répondre à la question réelle:

La pire et la plus courante des mauvaises pratiques C++ est probablement de le traiter comme C. L'utilisation continue des fonctions de l'API C comme printf, gets (également considérée comme mauvaise en C), strtok, etc ... non seulement ne parvient pas à tirer parti de la puissance fournie par le système de type plus serré, ils conduisent inévitablement à des complications supplémentaires lorsque l'on essaie d'interagir avec du "vrai" code C++. Donc, fondamentalement, faites exactement le contraire de ce que conseille Torvalds.

Apprenez à tirer parti de la STL et de Boost pour obtenir une détection plus approfondie des bogues au moment de la compilation et pour vous faciliter la vie par d'autres moyens généraux (le jeton de boost, par exemple, est à la fois sûr pour le type ET une meilleure interface). Il est vrai que vous devrez apprendre à lire les erreurs de modèle, ce qui est intimidant au début, mais (selon mon expérience en tout cas), c'est franchement beaucoup plus facile que d'essayer de déboguer quelque chose qui génère un comportement indéfini pendant l'exécution, ce que fait l'api C assez facile à faire.

Pour ne pas dire que C n'est pas aussi bon. Bien sûr, j'aime mieux C++. Les programmeurs C aiment mieux C. Il y a des compromis et des goûts subjectifs en jeu. Il y a aussi beaucoup de désinformation et de FUD flottant. Je dirais qu'il y a plus de FUD et de désinformation flottant autour de C++ mais je suis biaisé à cet égard. Par exemple, les problèmes de "ballonnement" et de "performances" supposés du C++ ne sont en fait pas des problèmes majeurs la plupart du temps et sont certainement hors de proportion avec la réalité.

Quant aux problèmes auxquels votre professeur fait référence, ils ne sont pas spécifiques au C++. Dans OOP (et dans la programmation générique), vous voulez préférer la composition à l'héritage. L'héritage est la relation de couplage la plus forte qui existe dans tous les langages OO. C++ ajoute une autre qui est plus forte, l'amitié. L'héritage polymorphe devrait être utilisé pour représenter les abstractions et les relations "est-un", il ne devrait jamais être utilisé pour la réutilisation. C'est la deuxième plus grande erreur que vous pouvez faire en C++, et c'est une très grosse erreur , mais il est loin d'être unique au langage. Vous pouvez créer des relations d'héritage trop complexes en C # ou Java aussi, et ils auront exactement les mêmes problèmes.

69
Edward Strange

J'ai toujours pensé que les dangers du C++ étaient très exagérés par les programmeurs C with Classes inexpérimentés.

Oui, C++ est plus difficile à maîtriser que quelque chose comme Java, mais si vous programmez en utilisant des techniques modernes, il est assez facile d'écrire des programmes robustes. Honnêtement, je n'ai pas ça beaucoup plus difficile de programmer en C++ que dans des langages comme Java, et je me retrouve souvent à manquer certaines abstractions C++ comme les modèles et RAII lorsque je conçois dans d'autres langages .

Cela dit, même après des années de programmation en C++, je ferai de temps en temps une erreur vraiment stupide qui ne serait pas possible dans un langage de niveau supérieur. Un écueil courant en C++ ignore la durée de vie des objets: en Java et C #, vous n'avez généralement pas à vous soucier de la durée de vie des objets *, car tous les objets existent sur le tas et ils sont gérés pour vous par un ramasse-miettes magique.

Maintenant, en C++ moderne, généralement vous n'avez pas non plus à vous soucier de la durée de vie des objets. Vous disposez de destructeurs et de pointeurs intelligents qui gèrent pour vous la durée de vie des objets. 99% du temps, cela fonctionne à merveille. Mais de temps en temps, vous serez foutu par un pointeur (ou une référence). Par exemple, récemment, j'avais un objet (appelons-le Foo) qui contenait une variable de référence interne vers un autre objet ( appelons-le Bar). À un moment donné, j'ai bêtement arrangé les choses pour que Bar soit hors de portée avant Foo, mais le destructeur de Foo a fini par appeler une fonction membre de Bar. Inutile de dire que les choses ne se sont pas bien passées.

Maintenant, je ne peux pas vraiment blâmer C++ pour cela. C'était ma propre mauvaise conception, mais le fait est que ce genre de chose ne se produirait pas dans un langage géré de niveau supérieur. Même avec des pointeurs intelligents et similaires, vous devez parfois avoir une conscience de la durée de vie des objets.


* Si la ressource gérée est de la mémoire, c'est-à-dire.

19
Charles Salvia

Surutilisation de try/catch blocs.

File file("some.txt");
try
{
  /**/

  file.close();
}
catch(std::exception const& e)
{
  file.close();
}

Cela provient généralement de langages comme Java et les gens diront que C++ n'a pas de clause finalize.

Mais ce code présente deux problèmes:

  • Il faut construire file avant le try/catch, car vous ne pouvez pas réellement close un fichier qui n'existe pas dans catch. Cela conduit à une "fuite de portée" en ce que file est visible après avoir été fermé. Vous pouvez ajouter un bloc mais ...: /
  • Si quelqu'un arrive et ajoute une return au milieu de la portée try, alors le fichier n'est pas fermé (c'est pourquoi les gens se moquent de l'absence de clause finalize)

Cependant, en C++, nous avons des moyens beaucoup plus efficaces de traiter ce problème qui:

  • finalize de Java
  • using de C #
  • Allez defer

Nous avons RAII, dont la propriété vraiment intéressante se résume le mieux à SBRM (Scoped Bound Resources Management).

En concevant la classe pour que son destructeur nettoie les ressources qu'elle possède, nous ne mettons pas la responsabilité de la gestion de la ressource sur chacun de ses utilisateurs!

C'est la fonctionnalité qui me manque dans toute autre langue, et probablement celle qui est la plus oubliée.

La vérité est qu'il est rarement nécessaire même d'écrire un try/catch bloc en C++, à part au niveau supérieur pour éviter la terminaison sans se connecter.

13
Matthieu M.

La différence dans le code est généralement plus liée au programmeur qu'à la langue. En particulier, un bon programmeur C++ et un programmeur C trouveront tous deux de bonnes solutions (même si différentes). Maintenant, C est un langage plus simple (en tant que langage) et cela signifie qu'il y a moins d'abstractions et plus de visibilité sur ce que fait réellement le code.

Une partie de sa diatribe (il est connu pour ses diatribes contre C++) est basée sur le fait que plus de gens vont adopter C++ et écrire du code sans réellement comprendre ce que certaines abstractions cachent et font de fausses hypothèses.

Une erreur courante qui correspond à vos critères est de ne pas comprendre comment fonctionnent les constructeurs de copie lors de l'utilisation de la mémoire allouée dans votre classe. J'ai perdu le compte du temps que j'ai passé à réparer les plantages ou les fuites de mémoire, car un `` noob '' a placé ses objets dans une carte ou un vecteur et n'a pas écrit correctement les constructeurs et destructeurs de copie.

Malheureusement, C++ regorge de pièges "cachés" comme celui-ci. Mais s'en plaindre, c'est comme se plaindre que vous êtes allé en France et que vous ne compreniez pas ce que les gens disaient. Si vous allez y aller, apprenez la langue.

9
Henry

C++ permet une grande variété de fonctionnalités et de styles de programmation, mais cela ne signifie pas que ce sont de bonnes façons d'utiliser C++. Et en fait, il est incroyablement facile d'utiliser incorrectement C++.

Il doit être appris et compris correctement , simplement apprendre en faisant (ou en l'utilisant comme on utiliserait un autre langage) conduira à un code inefficace et sujet aux erreurs.

6
Dario

Eh bien ... Pour commencer, vous pouvez lire le C++ FAQ Lite

Ensuite, plusieurs personnes ont construit des carrières en écrivant des livres sur les subtilités du C++:

Herb Sutter et Scott Meyers à savoir.

Quant à la diatribe de Torvalds qui manque de substance ... allez aux gens, sérieusement: aucune autre langue n'a eu autant d'encre sur le traitement des nuances de la langue. Vos livres Python & Ruby & Java se concentrent tous sur l'écriture d'applications ... vos livres C++ se concentrent sur les fonctionnalités idiotes du langage)/conseils/pièges.

4
red-dirt

Des modèles trop lourds peuvent ne pas entraîner de bogues au début. Au fil du temps, cependant, les gens devront modifier ce code, et ils auront du mal à comprendre un énorme modèle. C'est à ce moment que les bogues entrent - un malentendu provoque des commentaires "Il compile et exécute", ce qui conduit souvent à un code presque mais pas tout à fait correct.

Généralement, si je me vois faire un modèle générique profond à trois niveaux, je m'arrête et je pense comment le réduire à un. Souvent, le problème est résolu en extrayant des fonctions ou des classes.

3
Michael K

Attention: ce n'est pas autant une réponse qu'une critique de la conversation à laquelle "l'utilisateur inconnu" a lié dans sa réponse.

Son premier point principal est la (soi-disant) "norme en constante évolution". En réalité, les exemples qu'il donne se rapportent tous à des changements en C++ avant qu'il y ait un standard. Depuis 1998 (lorsque la première norme C++ a été finalisée), les modifications apportées au langage ont été assez minimes - en fait, beaucoup diraient que le vrai problème est que plus des modifications auraient dû être apportées. Je suis raisonnablement certain que tout le code conforme à la norme C++ d'origine est toujours conforme à la norme actuelle. Bien que ce soit quelque peu moins certain, à moins que quelque chose ne change rapidement (et de manière tout à fait inattendue), il en sera de même pour la prochaine norme C++ (théoriquement, tout le code qui utilisait export tombera en panne, mais pratiquement aucun n'existe; d'un point de vue pratique, ce n'est pas un problème). Je peux penser à quelques autres langues, systèmes d'exploitation (ou à beaucoup d'autres choses liées à l'ordinateur) qui peuvent faire une telle réclamation.

Il se lance ensuite dans "des styles en constante évolution". Encore une fois, la plupart de ses points sont assez proches du non-sens. Il essaie de caractériser for (int i=0; i<n;i++) comme "vieux et éclaté" et for (int i(0); i!=n;++i) "nouveau hotness". La réalité est que même s'il existe des types pour lesquels de telles modifications peuvent avoir un sens, pour int, cela ne fait aucune différence - et même lorsque vous pouvez gagner quelque chose, il est rarement nécessaire d'écrire du code correct ou correct. Même au mieux, il fait une montagne d'une taupinière.

Sa prochaine affirmation est que C++ "optimise dans la mauvaise direction" - en particulier, même s'il admet que l'utilisation de bonnes bibliothèques est facile, il affirme que C++ "rend presque impossible l'écriture de bonnes bibliothèques". Ici, je crois que c'est l'une de ses erreurs les plus fondamentales. En réalité, écrire de bonnes bibliothèques pour presque n'importe quelle langue est extrêmement difficile. Au minimum, l'écriture d'une bonne bibliothèque nécessite de comprendre si bien un domaine problématique que votre code fonctionne pour une multitude d'applications possibles dans (ou liées à) ce domaine. La plupart de ce que fait réellement C++ est de "relever la barre" - après avoir vu à quel point une bibliothèque peut mieux soit, les gens sont rarement prêts à recommencer à écrire le genre de dreck qu'ils auraient autrement. Il ignore également le fait que quelques vraiment bons codeurs écrivent pas mal de bibliothèques, qui peuvent ensuite être utilisées (facilement, comme il l'admet) par "the nous autres ". C'est vraiment un cas où "ce n'est pas un bug, c'est une fonctionnalité".

Je n'essaierai pas de toucher chaque point dans l'ordre (cela prendrait des pages), mais je vais directement à son point de clôture. Il cite Bjarne: "L'optimisation du programme entier peut être utilisée pour éliminer les tables de fonctions virtuelles et les données RTTI inutilisées. Une telle analyse est particulièrement adaptée aux programmes relativement petits qui n'utilisent pas de liaison dynamique."

Il critique cela en faisant une affirmation non étayée que "c'est un vraiment problème difficile", allant même jusqu'à le comparer au problème d'arrêt. En réalité, ce n'est rien de tel - en fait, l'éditeur de liens inclus avec Zortech C++ (à peu près le premier compilateur C++ pour MS-DOS, retour dans les années 80). Il est vrai qu'il est difficile d'être certain que chaque bit de données potentiellement étrangères a été éliminé, mais il est tout à fait raisonnable de faire un travail assez équitable.

Indépendamment de cela, cependant, le point beaucoup plus important est que cela n'est absolument pas pertinent pour la plupart des programmeurs dans tous les cas. Comme ceux d'entre nous qui ont désassemblé un peu de code le savent, à moins que vous n'écriviez du langage d'assemblage sans bibliothèque, vos exécutables contiennent presque certainement une bonne quantité de "trucs" (code et données, dans des cas typiques) que vous probablement même pas au courant, sans parler de l'utilisation réelle. Pour la plupart des gens, la plupart du temps, cela n'a pas d'importance - à moins que vous ne développiez pour les plus petits systèmes embarqués, cette consommation de stockage supplémentaire est tout simplement hors de propos.

En fin de compte, il est vrai que cette diatribe a un peu plus de substance que l'idiotie de Linus - mais cela lui donne exactement la damnation avec de faibles éloges qu'elle mérite.

2
Jerry Coffin

En tant que programmeur C qui a dû coder en C++ en raison de circonstances inévitables, voici mon expérience. Il y a très peu de choses que j'utilise en C++ et je m'en tiens principalement à C. La raison principale est que je ne comprends pas très bien le C++. Je n'avais/n'ai pas de mentor pour me montrer les subtilités du C++ et comment y écrire du bon code. Et sans l'aide d'un très bon code C++, il est extrêmement difficile d'écrire du bon code en C++. À mon humble avis, c'est le plus gros inconvénient de C++ parce que les bons codeurs C++ désireux de tenir les débutants sont difficiles à trouver.

Certains des résultats de performance que j'ai vus sont généralement dus à l'allocation magique de mémoire de STL (oui, vous pouvez changer l'allocateur, mais qui le fait quand il commence avec C++?). Vous entendez généralement des arguments d'experts C++ selon lesquels les vecteurs et les tableaux offrent des performances similaires, car les vecteurs utilisent les tableaux en interne et l'abstraction est super efficace. J'ai trouvé que cela était vrai dans la pratique pour l'accès aux vecteurs et la modification des valeurs existantes. Mais ce n'est pas vrai pour l'ajout d'une nouvelle entrée, la construction et la destruction de vecteurs. gprof a montré que 25% du temps cumulé pour une application était consacré aux constructeurs vectoriels, destructeurs, memmove (pour la relocalisation du vecteur entier pour ajouter un nouvel élément) et d'autres opérateurs vectoriels surchargés (comme ++).

Dans la même application, le vecteur de somethingSmall a été utilisé pour représenter un somethingBig. Il n'était pas nécessaire d'avoir un accès aléatoire à quelque chose de petit dans quelque chose de grand. Un vecteur a toujours été utilisé à la place d'une liste. La raison pour laquelle le vecteur a été utilisé? Parce que le codeur d'origine était familier avec la syntaxe de type tableau des vecteurs et pas très familier avec les itérateurs nécessaires pour les listes (oui, il vient d'un arrière-plan C). Continue à prouver que beaucoup de conseils d'experts sont nécessaires pour obtenir le bon C++. C offre si peu de constructions de base sans aucune abstraction, que vous pouvez faire les choses bien plus facilement que C++.

1
aufather

Bien que j'aime Linus Thorvalds, cette diatribe est sans substance - juste une diatribe.

Si vous aimez voir une diatribe corroborée, en voici une: "Pourquoi le C++ est mauvais pour l'environnement, provoque le réchauffement climatique et tue les chiots" http://chaosradio.ccc.de/camp2007_m4v_1951.html Additional matériel: http://www.fefe.de/c++/

Une conversation divertissante, à mon humble avis

0
user unknown

STL et boost sont portables, au niveau du code source. Je suppose que ce dont Linus parle, c'est que C++ n'a pas d'ABI (interface binaire d'application). Vous devez donc compiler toutes les bibliothèques avec lesquelles vous vous liez, avec la même version du compilateur et avec les mêmes commutateurs, ou bien limitez-vous au C ABI dans les bibliothèques de DLL. Je trouve également que annyoing .. mais à moins que vous ne fassiez des bibliothèques tierces, vous devriez pouvoir prendre le contrôle de votre environnement de construction. Je trouve que me limiter au C ABI ne vaut pas la peine. La commodité de pouvoir passer des chaînes, des vecteurs et des pointeurs intelligents d'une DLL à l'autre vaut la peine d'avoir à reconstruire toutes les bibliothèques lors de la mise à niveau des compilateurs ou du changement de commutateurs de compilateur. Les règles d'or que je suis sont les suivantes:

-Hériter de réutiliser l'interface, pas la mise en œuvre

-Préférer l'agrégation sur l'héritage

-Préférer autant que possible les fonctions libres aux méthodes membres

-Utilisez toujours l'idiome RAII pour sécuriser fortement votre code. Évitez d'essayer d'attraper.

-Utilisez des pointeurs intelligents, évitez les pointeurs nus (sans propriétaire)

-Préférer la sémantique des valeurs à la sémantique de référence

-Ne réinvente pas la roue, utilise stl et boost

-Utilisez l'idiome Pimpl pour masquer le privé et/ou pour fournir un pare-feu de compilateur

0
user16642