it-swarm.dev

Schlechteste Praktiken in C ++, häufige Fehler

Nachdem ich dieses berühmte Geschwätz von Linus Torvalds gelesen hatte, fragte ich mich, was eigentlich alle Fallstricke für Programmierer in C++ sind. Ich beziehe mich ausdrücklich nicht auf Tippfehler oder schlechten Programmablauf, wie in diese Frage und ihre Antworten behandelt, sondern auf übergeordnete Fehler, die vom Compiler nicht erkannt werden und nicht offensichtlich sind Fehler auf den ersten Blick, vollständige Designfehler, Dinge, die in C unwahrscheinlich sind, aber wahrscheinlich in C++ von Neulingen ausgeführt werden, die die vollständigen Auswirkungen ihres Codes nicht verstehen.

Ich begrüße auch Antworten, die auf einen enormen Leistungsabfall hinweisen, der normalerweise nicht zu erwarten ist. Ein Beispiel dafür, was mir einer meiner Professoren einmal über einen LR (1) -Parser-Generator erzählt hat, den ich geschrieben habe:

Sie haben etwas zu viele Instanzen nicht benötigter Vererbung und Virtualität verwendet. Vererbung macht ein Design viel komplizierter (und aufgrund des RTTI-Subsystems (Runtime Type Inference) ineffizient) und sollte daher nur dort verwendet werden, wo es sinnvoll ist, z. für die Aktionen in der Analysetabelle. Da Sie Vorlagen intensiv nutzen, benötigen Sie praktisch keine Vererbung. "

35
Felix Dombek

Torvalds redet hier aus seinem Arsch.


OK, warum redet er aus seinem Arsch:

Zuallererst ist sein Schimpfen wirklich nichts, ABER Schimpfen. Hier gibt es sehr wenig tatsächlichen Inhalt. Der einzige Grund, warum es wirklich berühmt ist oder sogar leicht respektiert wird, ist, dass es vom Linux-Gott gemacht wurde. Sein Hauptargument ist, dass C++ Mist ist und er C++ Leute gerne verärgert. Es gibt natürlich überhaupt keinen Grund, darauf zu antworten, und jeder, der es für ein vernünftiges Argument hält, ist sowieso nicht zu sprechen.

Was als seine objektivsten Punkte angesehen werden könnte:

  • STL und Boost sind völliger Mist. Du bist ein Idiot.
  • STL und Boost verursachen unendlich viele Schmerzen <- lächerlich. Offensichtlich übertreibt er absichtlich, aber was ist dann seine wahre Aussage hier? Ich weiß es nicht. Es gibt einige mehr als trivial schwierige Probleme, wenn Sie in Spirit oder so etwas Compiler-Erbrechen verursachen, aber es ist nicht mehr oder weniger schwierig, dies herauszufinden, als UB zu debuggen, das durch den Missbrauch von C-Konstrukten wie void * verursacht wird.
  • Von C++ ermutigte abstrakte Modelle sind ineffizient. <- Wie was? Er erweitert nie, liefert nie Beispiele dafür, was er meint, er sagt es nur. BFD. Da ich nicht sagen kann, worauf er sich bezieht, macht es wenig Sinn, die Aussage zu "widerlegen". Es ist ein allgemeines Mantra von C-Bigots, aber das macht es nicht verständlicher oder verständlicher.
  • Die korrekte Verwendung von C++ bedeutet, dass Sie sich auf die C-Aspekte beschränken. <- Eigentlich macht der WORSE C++ - Code da draußen das, also weiß ich immer noch nicht, über welche WTF er spricht.

Grundsätzlich spricht Torvalds aus seinem Arsch. Es gibt keine verständlichen Argumente für irgendetwas. Eine ernsthafte Widerlegung eines solchen Unsinns zu erwarten, ist einfach albern. Mir wird gesagt, ich solle eine Widerlegung von etwas "erweitern", von dem ich erwarten würde, dass es erweitert wird, wenn es dort ist, wo ich es gesagt habe. Wenn Sie sich wirklich ehrlich ansehen, was Torvalds gesagt hat, werden Sie sehen, dass er eigentlich nichts gesagt hat.

Nur weil Gott sagt, dass es keinen Sinn macht oder ernst genommen werden sollte, als wenn es ein zufälliger Bozo gesagt hätte. Um ehrlich zu sein, Gott ist nur ein weiterer zufälliger Bozo.


Antwort auf die eigentliche Frage:

Die wahrscheinlich schlechteste und häufigste schlechte C++ - Praxis besteht darin, sie wie C zu behandeln. Die fortgesetzte Verwendung von C-API-Funktionen wie printf, gets (auch in C als schlecht angesehen), strtok usw. kann nicht nur die bereitgestellte Leistung nicht nutzen Durch das engere Typsystem führen sie unweigerlich zu weiteren Komplikationen beim Versuch, mit "echtem" C++ - Code zu interagieren. Machen Sie also genau das Gegenteil von dem, was Torvalds empfiehlt.

Erfahren Sie, wie Sie STL und Boost nutzen, um Fehler in der Kompilierungszeit weiter zu erkennen und Ihr Leben auf andere, allgemeine Weise zu vereinfachen (der Boost-Tokenizer ist beispielsweise sowohl typsicher als auch eine bessere Benutzeroberfläche). Es ist wahr, dass Sie lernen müssen, wie man Vorlagenfehler liest, was zunächst entmutigend ist, aber (meiner Erfahrung nach jedenfalls) ist es ehrlich gesagt viel einfacher, als zu versuchen, etwas zu debuggen, das zur Laufzeit undefiniertes Verhalten erzeugt, das die API macht ganz einfach zu machen.

Um nicht zu sagen, dass C nicht so gut ist. Ich mag C++ natürlich besser. C-Programmierer mögen C besser. Es gibt Kompromisse und subjektive Vorlieben. Es gibt auch viele Fehlinformationen und FUD. Ich würde sagen, dass es mehr FUD und Fehlinformationen über C++ gibt, aber ich bin in dieser Hinsicht voreingenommen. Zum Beispiel sind die Probleme "Aufblähen" und "Leistung", die C++ angeblich hat, die meiste Zeit keine großen Probleme und werden sicherlich aus den Proportionen der Realität herausgeblasen.

Die Themen, auf die sich Ihr Professor bezieht, gelten nicht nur für C++. In OOP (und in der generischen Programmierung)) möchten Sie die Komposition der Vererbung vorziehen. Die Vererbung ist die stärkste mögliche Kopplungsbeziehung, die in allen OO - Sprachen besteht. C++ fügt hinzu eine weitere, die stärker ist, Freundschaft. Polymorphe Vererbung sollte verwendet werden, um Abstraktionen und "is-a" -Beziehungen darzustellen. Sie sollte niemals zur Wiederverwendung verwendet werden. Dies ist der zweitgrößte Fehler, den Sie in C++ machen können, und es ist ein ziemlich großer Sie können jedoch in C # oder Java auch) übermäßig komplexe Vererbungsbeziehungen erstellen, und sie haben genau die gleichen Probleme.

69
Edward Strange

Ich habe immer gedacht, dass die Gefahren von C++ von unerfahrenen C mit Klassenprogrammierern stark übertrieben wurden.

Ja, C++ ist schwieriger zu erlernen als Java, aber wenn Sie mit modernen Techniken programmieren, ist es ziemlich einfach, robuste Programme zu schreiben. Ich habe ehrlich gesagt nicht , dass eine viel schwierigere Zeitprogrammierung in C++ als in Sprachen wie Java ist, und ich finde oft, dass mir bestimmte C++ fehlen Abstraktionen wie Vorlagen und RAII, wenn ich in anderen Sprachen entwerfe.

Trotzdem mache ich auch nach Jahren des Programmierens in C++ hin und wieder einen wirklich dummen Fehler, der in einer höheren Sprache nicht möglich wäre. Eine häufige Gefahr in C++ ist das Ignorieren der Objektlebensdauer: In Java und C # müssen Sie sich im Allgemeinen nicht um die Objektlebensdauer * kümmern, da alle Objekte auf dem Heap vorhanden sind und für Sie verwaltet werden von einem magischen Müllsammler.

In modernem C++ müssen Sie sich normalerweise auch nicht viel um die Lebensdauer von Objekten kümmern. Sie haben Destruktoren und intelligente Zeiger, die die Lebensdauer von Objekten für Sie verwalten. In 99% der Fälle funktioniert dies wunderbar. Aber hin und wieder werden Sie von einem baumelnden Zeiger (oder einer Referenz) verarscht. Zum Beispiel hatte ich erst kürzlich ein Objekt (nennen wir es Foo), das eine interne Referenzvariable für ein anderes Objekt enthielt ( Nennen wir es Bar). Irgendwann habe ich die Dinge dumm angeordnet, so dass Bar vor Foo den Gültigkeitsbereich verließ, aber der Destruktor von Foo schließlich eine Mitgliedsfunktion von Bar aufrief. Unnötig zu erwähnen, dass die Dinge nicht gut liefen.

Jetzt kann ich C++ nicht wirklich dafür verantwortlich machen. Es war mein eigenes schlechtes Design, aber der Punkt ist, dass so etwas in einer übergeordneten, verwalteten Sprache nicht passieren würde. Selbst mit intelligenten Zeigern und dergleichen müssen Sie manchmal noch ein Bewusstsein für die Lebensdauer des Objekts haben.


* Wenn es sich bei der verwalteten Ressource um Speicher handelt.

19
Charles Salvia

Überbeanspruchung von try/catch Blöcke.

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

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

Dies ergibt sich normalerweise aus Sprachen wie Java und die Leute werden argumentieren, dass C++ eine finalize -Klausel fehlt.

Dieser Code weist jedoch zwei Probleme auf:

  • Man muss file vor dem try/catch, weil Sie nicht close eine Datei können, die in catch nicht vorhanden ist. Dies führt zu einem "Scope Leak", da file nach dem Schließen sichtbar ist. Sie können einen Block hinzufügen, aber ...: /
  • Wenn jemand vorbeikommt und ein return in der Mitte des Bereichs try hinzufügt, wird die Datei nicht geschlossen (weshalb die Leute über das Fehlen der Klausel finalize meckern).

In C++ haben wir jedoch viel effizientere Möglichkeiten, um dieses Problem zu lösen:

  • Javas finalize
  • C # 's using
  • Go's defer

Wir haben RAII, dessen wirklich interessante Eigenschaft am besten als SBRM (Scoped Bound Resources Management) zusammengefasst wird.

Indem wir die Klasse so gestalten, dass ihr Destruktor die Ressourcen bereinigt, die sie besitzt, müssen wir die Ressource nicht jedem einzelnen Benutzer verwalten!

Dies ist die Funktion, die ich in jeder anderen Sprache vermisse und wahrscheinlich die, die am meisten vergessen wird.

Die Wahrheit ist, dass es selten notwendig ist, ein try/catch Block in C++, abgesehen von der obersten Ebene, um eine Beendigung ohne Protokollierung zu vermeiden.

13
Matthieu M.

Der Unterschied im Code hängt normalerweise mehr mit dem Programmierer als mit der Sprache zusammen. Insbesondere ein guter C++ - Programmierer und ein C-Programmierer werden beide zu ähnlich guten (auch wenn unterschiedlichen) Lösungen kommen. Jetzt ist C eine einfachere Sprache (als Sprache) und das bedeutet, dass es weniger Abstraktionen und mehr Sichtbarkeit darüber gibt, was der Code tatsächlich tut.

Ein Teil seiner Beschimpfungen (er ist bekannt für seine Beschimpfungen gegen C++) basiert auf der Tatsache, dass mehr Leute C++ übernehmen und Code schreiben, ohne wirklich zu verstehen, was einige der Abstraktionen verbergen und falsche Annahmen treffen.

Ein häufiger Fehler, der Ihren Kriterien entspricht, besteht darin, nicht zu verstehen, wie Kopierkonstruktoren beim Umgang mit zugewiesenem Speicher in Ihrer Klasse funktionieren. Ich habe die Zeit verloren, die ich für die Behebung von Abstürzen oder Speicherlecks aufgewendet habe, weil ein "Noob" seine Objekte in eine Karte oder einen Vektor eingefügt und Kopierkonstruktoren und Destruktoren nicht richtig geschrieben hat.

Leider ist C++ voll von solchen 'versteckten' Fallstricken. Aber sich darüber zu beschweren ist wie sich zu beschweren, dass Sie nach Frankreich gegangen sind und nicht verstehen konnten, was die Leute sagten. Wenn Sie dorthin gehen, lernen Sie die Sprache.

9
Henry

C++ ermöglicht eine Vielzahl von Funktionen und Programmierstilen, aber das bedeutet nicht, dass dies tatsächlich gute Möglichkeiten für die Verwendung von C++ sind. Tatsächlich ist es unglaublich einfach, C++ falsch zu verwenden.

Es muss richtig gelernt und verstanden werden sein. Nur das Lernen durch Tun (oder Verwenden, als würde man eine andere Sprache verwenden) führt zu ineffizientem und fehleranfälligem Code.

6
Dario

Nun ... Für den Anfang können Sie die C++ FAQ Lite lesen

Dann haben mehrere Leute Karrieren aufgebaut und Bücher über die Feinheiten von C++ geschrieben:

Herb Sutter und Scott Meyers nämlich.

Was Torvalds 'schimpfende Substanz angeht ... kommen Sie ernsthaft zu den Menschen: In keiner anderen Sprache wurde so viel Tinte verschüttet, um mit den Nuancen der Sprache umzugehen. Ihre Python & Ruby & Java) Bücher konzentrieren sich alle auf das Schreiben von Anwendungen ... Ihre C++ - Bücher konzentrieren sich auf alberne Sprachfunktionen/Tipps/Fallen.

4
red-dirt

Zu starkes Templating führt möglicherweise zunächst nicht zu Fehlern. Im Laufe der Zeit müssen die Benutzer diesen Code jedoch ändern, und es wird ihnen schwer fallen, eine riesige Vorlage zu verstehen. Dann treten Fehler auf - Missverständnisse verursachen "Es wird kompiliert und ausgeführt" -Kommentare, die häufig zu fast, aber nicht ganz korrektem Code führen.

Wenn ich sehe, dass ich eine dreistufige generische Vorlage mache, halte ich im Allgemeinen inne und überlege, wie sie auf eine reduziert werden könnte. Oft wird das Problem durch Extrahieren von Funktionen oder Klassen gelöst.

3
Michael K

Warnung: Dies ist bei weitem keine so große Antwort wie eine Kritik an dem Vortrag, mit dem "unbekannter Benutzer" in seiner Antwort verknüpft ist.

Sein erster Hauptpunkt ist der (angeblich) "sich ständig ändernde Standard". In Wirklichkeit beziehen sich die Beispiele, die er gibt, alle auf Änderungen in C++ , bevor es einen Standard gab. Seit 1998 (als der erste C++ - Standard fertiggestellt wurde) waren die Änderungen an der Sprache recht gering - tatsächlich würden viele argumentieren, dass das eigentliche Problem darin besteht, dass mehr Änderungen sollten vorgenommen worden sein. Ich bin mir ziemlich sicher, dass der gesamte Code, der dem ursprünglichen C++ - Standard entspricht, immer noch dem aktuellen Standard entspricht. Obwohl es etwas weniger sicher ist, gilt das Gleiche auch für den kommenden C++ - Standard (theoretisch,), sofern sich nichts schnell (und ganz unerwartet) ändert. Der gesamte Code, der export verwendet hat, wird beschädigt, aber es gibt praktisch keinen (aus praktischer Sicht ist dies kein Problem). Ich kann mir nur wenige andere Sprachen, Betriebssysteme (oder vieles andere, was mit Computern zu tun hat) vorstellen, die einen solchen Anspruch erheben können.

Er geht dann in "ständig wechselnde Stile". Auch hier sind die meisten seiner Punkte dem Unsinn ziemlich nahe. Er versucht, for (int i=0; i<n;i++) als "alt und kaputt" und for (int i(0); i!=n;++i) als "neue Schärfe" zu charakterisieren. Die Realität ist, dass es zwar Typen gibt, für die solche Änderungen sinnvoll sein könnten, aber für int keinen Unterschied macht - und selbst wenn Sie etwas gewinnen könnten, ist es selten notwendig, guten oder korrekten Code zu schreiben. Selbst im besten Fall macht er aus einem Maulwurfshügel einen Berg.

Seine nächste Behauptung ist, dass C++ "in die falsche Richtung optimiert" - insbesondere, dass er zwar zugibt, dass die Verwendung guter Bibliotheken einfach ist, C++ jedoch "das Schreiben guter Bibliotheken fast unmöglich macht". Hier glaube ich, ist einer seiner grundlegendsten Fehler. In Wirklichkeit ist es äußerst schwierig, gute Bibliotheken für fast jede Sprache zu schreiben. Zum Schreiben einer guten Bibliothek muss zumindest eine Problemdomäne so gut verstanden werden, dass Ihr Code für eine Vielzahl möglicher Anwendungen in dieser Domäne (oder in Bezug auf diese Domäne) funktioniert. Das meiste, was C++ wirklich tut, ist "die Messlatte höher legen" - nachdem man gesehen hat, wie viel besser eine Bibliothek kann sein, die Leute sind selten bereit, wieder die Art von Dreck zu schreiben, die sie sonst hätten. Er ignoriert auch die Tatsache, dass einige wirklich gute Codierer einige Bibliotheken schreiben, die dann (leicht, wie er zugibt) von "the Wir Übrigen". Dies ist wirklich ein Fall, in dem "das kein Fehler ist, sondern eine Funktion".

Ich werde nicht versuchen, jeden Punkt der Reihe nach zu treffen (das würde Seiten dauern), sondern direkt zu seinem Schlusspunkt springen. Er zitiert Bjarne mit den Worten: "Mit der Optimierung des gesamten Programms können nicht verwendete virtuelle Funktionstabellen und RTTI-Daten entfernt werden. Eine solche Analyse eignet sich besonders für relativ kleine Programme, die keine dynamische Verknüpfung verwenden."

Er kritisiert dies, indem er eine nicht unterstützte Behauptung aufstellt, dass "dies ein wirklich schweres Problem ist", und geht sogar so weit, es mit dem Stopp-Problem zu vergleichen. In Wirklichkeit ist es nichts dergleichen - tatsächlich ist der in Zortech C++ enthaltene Linker (so ziemlich der erste C++ - Compiler für MS-DOS, zurück in den 1980er Jahren) tat dies. Es ist wahr, dass es schwierig ist, sicher zu sein, dass alle möglicherweise irrelevanten Daten eliminiert wurden, aber immer noch völlig vernünftig, um einen ziemlich fairen Job zu machen.

Unabhängig davon ist der viel wichtigere Punkt jedoch, dass dies für die meisten Programmierer auf jeden Fall völlig irrelevant ist. Wie diejenigen von uns, die ziemlich viel Code zerlegt haben, wissen, enthalten Ihre ausführbaren Dateien mit ziemlicher Sicherheit eine ganze Menge "Zeug" (in typischen Fällen sowohl Code als auch Daten), das Sie haben, es sei denn, Sie schreiben Assemblersprache ohne Bibliotheken wahrscheinlich nicht einmal wissen, ganz zu schweigen davon, jemals tatsächlich zu verwenden. Für die meisten Menschen spielt es meistens keine Rolle - es sei denn, Sie entwickeln für die kleinsten eingebetteten Systeme, ist dieser zusätzliche Speicherverbrauch einfach irrelevant.

Am Ende ist es wahr, dass dieses Geschwätz etwas mehr Substanz hat als Linus 'Idiotie - aber das gibt ihm genau das Verdammnis mit einem schwachen Lob, das es verdient.

2
Jerry Coffin

Als C-Programmierer, der aufgrund unvermeidbarer Umstände in C++ programmieren musste, ist hier meine Erfahrung. Es gibt sehr wenige Dinge, die ich benutze, die C++ sind und meistens bei C bleiben. Der Hauptgrund ist, dass ich C++ nicht so gut verstehe. Ich hatte/hatte keinen Mentor, der mir die Feinheiten von C++ zeigte und wie man guten Code darin schreibt. Und ohne Anleitung durch einen sehr sehr guten C++ - Code ist es äußerst schwierig, guten Code in C++ zu schreiben. IMHO ist dies der größte Nachteil von C++, da gute C++ - Codierer, die bereit sind, Anfänger zu halten, schwer zu bekommen sind.

Einige der Performance-Hits, die ich normalerweise gesehen habe, sind auf die magische Speicherzuweisung von STL zurückzuführen (ja, Sie können den Allokator ändern, aber wer macht das, wenn er mit C++ beginnt?). Normalerweise hören Sie Argumente von C++ - Experten, dass Vektoren und Arrays eine ähnliche Leistung bieten, da Vektoren Arrays intern verwenden und die Abstraktion sehr effizient ist. Ich habe festgestellt, dass dies in der Praxis für den Vektorzugriff und das Ändern vorhandener Werte zutrifft. Dies gilt jedoch nicht für das Hinzufügen eines neuen Eintrags, die Konstruktion und die Zerstörung von Vektoren. gprof zeigte, dass insgesamt 25% der Zeit für eine Anwendung in Vektorkonstruktoren, Destruktoren, memmove (zum Verschieben des gesamten Vektors zum Hinzufügen eines neuen Elements) und anderen überladenen Vektoroperatoren (wie ++) verbracht wurden.

In derselben Anwendung wurde der Vektor von SomethingSmall verwendet, um etwas SomethingBig darzustellen. Es war kein zufälliger Zugriff auf etwas Kleines in etwas Großem erforderlich. Es wurde immer noch ein Vektor anstelle einer Liste verwendet. Der Grund, warum Vektor verwendet wurde? Weil der ursprüngliche Codierer mit der Array-ähnlichen Syntax von Vektoren vertraut war und mit den für Listen benötigten Iteratoren nicht sehr vertraut war (ja, er hat einen C-Hintergrund). Beweist weiter, dass viele Anleitungen von Experten erforderlich sind, um C++ richtig zu machen. C bietet so wenig grundlegende Konstrukte ohne jegliche Abstraktion, dass Sie es viel einfacher als C++ richtig machen können.

1
aufather

Obwohl ich Linus Thorvalds mag, ist dieses Geschwätz ohne Substanz - nur ein Geschwätz.

Wenn Sie eine fundierte Beschimpfung sehen möchten, finden Sie hier eine: "Warum C++ schlecht für die Umwelt ist, die globale Erwärmung verursacht und Welpen tötet" http://chaosradio.ccc.de/camp2007_m4v_1951.html Zusätzliche Material: http://www.fefe.de/c++/

Ein unterhaltsames Gespräch, imho

0
user unknown

STL und Boost sind auf Quellcodeebene portabel. Ich denke, Linus spricht davon, dass C++ kein ABI (Application Binary Interface) hat. Sie müssen also alle Bibliotheken kompilieren, mit denen Sie verknüpfen, mit derselben Compilerversion und mit denselben Switches, oder sich auf das C ABI an den DLL-Grenzen beschränken. Ich finde das auch ärgerlich. Aber wenn Sie keine Bibliotheken von Drittanbietern erstellen, sollten Sie in der Lage sein, die Kontrolle über Ihre Build-Umgebung zu übernehmen. Ich finde, die Beschränkung auf das C ABI ist die Mühe nicht wert. Die Bequemlichkeit, Zeichenfolgen, Vektoren und intelligente Zeiger von einer DLL zur anderen übergeben zu können, ist die Mühe wert, alle Bibliotheken neu zu erstellen, wenn Compiler aktualisiert oder Compiler-Switches geändert werden. Die goldenen Regeln, denen ich folge, sind:

-Erben Sie die Schnittstelle wieder, nicht die Implementierung

- Bevorzugen Sie die Aggregation gegenüber der Vererbung

- Bevorzugen Sie nach Möglichkeit freie Funktionen für Mitgliedsmethoden

- Verwenden Sie immer die RAII-Sprache, um Ihren Code ausnahmsicher zu machen. Vermeiden Sie es zu fangen.

- Verwenden Sie intelligente Zeiger und vermeiden Sie nackte (nicht besessene) Zeiger

- Ziehen Sie die Wertesemantik der Referenzsemantik vor

- Das Rad nicht neu erfinden, stl und Boost verwenden

- Verwenden Sie die Pimpl-Sprache, um private und/oder eine Compiler-Firewall bereitzustellen

0
user16642