Besteht ein echtes Risiko für die C ++ STL-Container?

Die Behauptung, dass es ein Fehler ist, jemals einen C ++ – Standardcontainer als Basisklasse zu verwenden, überrascht mich.

Wenn es keinen Missbrauch der Sprache zu deklarieren gibt …

// Example A typedef std::vector Rates; typedef std::vector Charges; 

… was genau ist dann die Gefahr bei der Erklärung …

 // Example B class Rates : public std::vector { // ... } ; class Charges: public std::vector { // ... } ; 

Zu den positiven Vorteilen von B gehören:

  • Aktiviert das Überladen von functionen, da f (Raten &) und f (Gebühren &) unterschiedliche Signaturen sind
  • Ermöglicht die Spezialisierung anderer Vorlagen, da X und X unterschiedliche Typen sind
  • Vorwärtsdeklaration ist trivial
  • Der Debugger sagt Ihnen wahrscheinlich, ob das Objekt ein Kurs oder eine Gebühr ist
  • Wenn im Laufe der Zeit die Tarife und Gebühren Persönlichkeiten entwickeln – ein Singleton for Rates, ein Ausgabeformat für Gebühren – gibt es einen offensichtlichen Spielraum für die Implementierung dieser functionalität.

Zu den positiven Vorteilen von A gehören:

  • Sie müssen keine trivialen Implementierungen von Konstruktoren usw. bereitstellen
  • Der fünfzehn Jahre alte Vor-Standard-Compiler, der das einzige ist, was Ihr Vermächtnis kompiliert, erstickt nicht
  • Da Spezialisierungen unmöglich sind, verwenden Template X und Template X denselben Code, also kein sinnloses Aufblähen.

Beide Ansätze sind der Verwendung eines rohen Containers überlegen, denn wenn sich die Implementierung vom Vektor zum Vektor ändert, gibt es nur einen Platz mit B zu ändern und vielleicht nur einen Platz mit A (es könnte mehr sein, weil jemand könnte identische typedef-statementen an mehreren Stellen platziert haben).

Mein Ziel ist, dass dies eine spezifische, beantwortbare Frage ist, keine Diskussion über bessere oder schlechtere Praxis. Zeigen Sie das Schlimmste, was als Folge eines Standardcontainers passieren kann, was durch die Verwendung von typedef verhindert worden wäre.

Bearbeiten:

Ohne Frage wäre das Hinzufügen eines Destruktors zur class Rate oder class Charge ein Risiko, da std :: vector seinen Destruktor nicht als virtuell deklariert. Im Beispiel gibt es keinen Destruktor und keinen Bedarf für einen. Durch das Zerstören eines Rates oder Charges-Objekts wird der Destruktor der Basisklasse aufgerufen. Hier ist auch kein Polymorphismus nötig. Die Herausforderung besteht darin, etwas Schlechtes zu zeigen, das als Konsequenz der Verwendung von Ableitung anstelle von typedef auftritt.

Bearbeiten:

Betrachten Sie diesen Anwendungsfall:

 #include  #include  void kill_it(std::vector *victim) { // user code, knows nothing of Rates or Charges // invokes non-virtual ~std::vector(), then frees the // memory allocated at address victim delete victim ; } typedef std::vector Rates; class Charges: public std::vector { }; int main(int, char **) { std::vector *p1, *p2; p1 = new Rates; p2 = new Charges; // ??? kill_it(p2); kill_it(p1); return 0; } 

Gibt es einen möglichen Fehler, den sogar ein willkürlich unglücklicher Benutzer in das einführen könnte? Abschnitt, der zu einem Problem mit Gebühren führen wird (die abgeleitete class), aber nicht mit Rates (the typedef)?

In der Microsoft-Implementierung wird Vektor selbst über inheritance implementiert. Vektor ist ein öffentlich abgeleitet von _Vector_Val Sollte Einschließung bevorzugt werden?

Die Standardcontainer haben keine virtuellen Destruktoren, daher können Sie sie nicht polymorph behandeln. Wenn Sie nicht, und jeder, der Ihren Code verwendet, nicht tut, ist es nicht “falsch” an sich. Allerdings ist es besser, die Komposition aus Gründen der Übersichtlichkeit zu verwenden.

Weil Sie einen virtuellen Destruktor benötigen und die Std-Container ihn nicht haben. Die Standardcontainer sind nicht als Basisklasse konzipiert.

Weitere Informationen finden Sie im Artikel “Warum sollten wir keine class von STL-classn erben?”

Richtlinie

Eine Basisklasse muss haben:

  • ein öffentlicher virtueller Destruktor
  • oder ein geschützter nicht-virtueller Destruktor

Ein starkes Gegenargument ist meiner Meinung nach, dass Sie Ihren Typen eine Schnittstelle und die Implementierung aufzwingen. Was passiert, wenn Sie herausfinden, dass die Vektorspeicherzuweisungsstrategie nicht Ihren Anforderungen entspricht? Werden Sie von std:deque ableiten? Was ist mit den 128 KB Codezeilen, die Ihre class bereits verwenden? Wird jeder alles neu kompilieren müssen? Wird es überhaupt kompilieren?

Das Problem ist kein philisophisches, sondern ein Implementierungsproblem. Die Destruktoren der Standardcontainer sind nicht virtuell, was bedeutet, dass es keine Möglichkeit gibt, die Laufzeitpolymorphismen zu verwenden, um den richtigen Destruktor zu erhalten.

Ich habe in der Praxis festgestellt, dass es wirklich nicht so schlimm ist, meine eigenen benutzerdefinierten Listenklassen nur mit den Methoden zu erstellen, die mein Code definiert (und ein privates Mitglied der “Eltern” -class). In der Tat führt es oft zu besser gestalteten classn.

Abgesehen von der Tatsache, dass eine Basisklasse einen virtuellen Destruktor oder einen geschützten nicht virtuellen Destruktor benötigt, machen Sie in Ihrem Design die folgende Assertion:

Preise und Gebühren für diese Angelegenheit sind die gleichen wie ein Vektor von Doppel in Ihrem Beispiel oben. Durch Ihre eigene Behauptung “… im Laufe der Zeit, Preise und Gebühren entwickeln Persönlichkeiten …” ist dann die Behauptung, dass die Preise sind immer noch als ein Vektor der Doppelgänger an dieser Stelle? Ein Vektor von Doubles ist kein Singleton. Wenn ich zum Beispiel die Preise verwende, um meinen Double-Vektor für Widgets zu deklarieren, kann mir der Code Kopfschmerzen bereiten. Was kann sonst noch unter Preise und Gebühren geändert werden? Werden Änderungen an der Basisklasse von Clients Ihres Designs sicher isoliert, wenn sie sich grundlegend ändern?

Der Punkt ist eine class ist ein Element, von vielen in C ++, um Designabsichten auszudrücken. Zu sagen, was Sie meinen und was Sie meinen, ist der Grund gegen die Verwendung von inheritance auf diese Weise.

… oder kurz vor meiner Antwort: Ersatz.

In den meisten Fällen sollten Sie auch die Zusammensetzung oder Aggregation nach inheritance bevorzugen.

Ein Wort: Substituierbarkeit

Gibt es einen möglichen Fehler, den sogar ein willkürlich unglücklicher Benutzer in das einführen könnte? Abschnitt, der zu einem Problem mit Gebühren führen wird (die abgeleitete class), aber nicht mit Rates (the typedef)?

Erstens, da ist Mankarses hervorragender Punkt:

Der Kommentar in kill_it ist falsch. Wenn der dynamische Typ des Opfers nicht std::vector , ruft das delete einfach undefiniertes Verhalten auf. Der Aufruf von kill_it(p2) bewirkt, dass dies geschieht, und daher muss nichts zu dem //??? hinzugefügt werden. Abschnitt dafür, undefiniertes Verhalten zu haben. – Mankarse 3. September 11 um 10:53

Zweitens sagen sie, dass sie f(*p1); wo f ist spezialisiert für std::vector : diese vector wird nicht gefunden – Sie können am Ende die Template-Spezialisierung anders – in der Regel läuft (langsamer oder sonst weniger effizient) generischen Code, oder bekommen einen Linker-Fehler wenn eine unspezialisierte Version ist nicht wirklich definiert. Nicht oft eine wesentliche Sorge.

Persönlich betrachte ich die Zerstörung durch einen pointers auf die Basis, um die Grenze zu kreuzen – es kann nur ein “hypothetisches” Problem sein (soweit Sie das beurteilen können), wenn Sie Ihren aktuellen Compiler, Compilerflag, Programm, Betriebssystemversion usw. verwenden könnte jederzeit aus keinem “guten” Grund brechen.

Wenn Sie sicher sind, dass Sie das Löschen über einen Basisklassenzeiger vermeiden können, gehen Sie dafür vor.

Dazu einige Anmerkungen zu Ihrer Einschätzung:

  • “Bereitstellung trivialer Implementierungen von Konstruktoren” – das ist ein Ärger, aber ein Tipp für C ++ 03: template Classname(const A& a) : Base(a) { } template Classname(const A& a, const B& b) : Base(a, b) { } ... kann manchmal einfacher sein als das Aufzählen aller Überladungen, aber nicht mit nichtkonstanten Parametern, Standardwerten, expliziten und nicht-expliziten Konstruktoren , noch auf eine große Anzahl von Argumenten skalieren. C ++ 11 bietet eine bessere allgemeine Lösung.

Ohne Frage wäre das Hinzufügen eines Destruktors zur class Rates oder class Charges ein Risiko, da std::vector seinen Destruktor nicht als virtuell deklariert. Im Beispiel gibt es keinen Destruktor und keinen Bedarf für einen. Durch das Zerstören eines Rates oder Charges-Objekts wird der Destruktor der Basisklasse aufgerufen. Hier ist auch kein Polymorphismus nötig.

  • Es besteht kein Risiko durch einen abgeleiteten classndestruktor, wenn das Objekt nicht polymorph gelöscht wird; wenn es ein nicht definiertes Verhalten gibt, ob Ihre abgeleitete class einen benutzerdefinierten Destruktor hat oder nicht. Das heißt, Sie gehen von “wahrscheinlich-ok-for-a-cowboy” zu “fast-sicher-nicht-ok”, wenn Sie Datenelemente oder weitere Basen mit Destruktoren hinzufügen, die Aufräumarbeiten durchführen (Speicherfreigabe, Mutex-Entsperrung, Datei) Griff schließen etc.)

  • Wenn man sagt “wird den Basisklassen-Destruktor aufrufen”, hört es sich so an, als ob es direkt ohne implizit definierten abgeleiteten class-Destruktor ausgeführt wird oder den Aufruf ausführt – alles ein Optimierungsdetail und nicht durch den Standard spezifiziert.