wake-up-neo.com

Warum sollte man nicht von der C++ - std-String-Klasse ableiten?

Ich wollte nach einem bestimmten Punkt in Effective C++ fragen. 

Es sagt:

Ein Destruktor sollte virtuell gemacht werden, wenn eine Klasse als polymorphe Klasse fungieren muss. Es fügt außerdem hinzu, dass std::string keinen virtuellen Destruktor hat und daher niemals davon abgeleitet werden sollte. Auch std::string ist nicht einmal als Basisklasse konzipiert, vergessen Sie die polymorphe Basisklasse. 

Ich verstehe nicht, was in einer Klasse konkret erforderlich ist, um eine Basisklasse zu sein (keine polymorphe)?

Ist der einzige Grund, aus dem ich nicht von der Klasse std::string abgeleitet werden sollte, ein virtueller Destruktor? Für die Wiederverwendbarkeit kann eine Basisklasse definiert werden, und mehrere abgeleitete Klassen können davon erben. Was macht std::string nicht einmal als Basisklasse geeignet?

Wenn es eine rein wiederverwendbare Basisklasse gibt und es viele abgeleitete Typen gibt, gibt es eine Möglichkeit, den Client daran zu hindern, Base* p = new Derived() auszuführen, weil die Klassen nicht polymorph verwendet werden sollen?

60

Ich denke, diese Aussage spiegelt die Verwirrung hier wider (Hervorhebung meines):

Ich verstehe nicht, was in einer Klasse konkret erforderlich ist, um als Basisklasse (keine polymorphe Klasse) geeignet zu sein.

In idiomatic C++ gibt es zwei Verwendungsmöglichkeiten für die Ableitung von einer Klasse:

  • private Vererbung, wird für Mixins und die aspektorientierte Programmierung mit Vorlagen verwendet.
  • public Vererbung, wird für nur polymorphe Situationen verwendet. EDIT: Okay, ich denke, das könnte auch in einigen Mixin-Szenarien verwendet werden - wie boost::iterator_facade -, die angezeigt werden, wenn CRTP verwendet wird.

Es gibt absolut keinen Grund, eine Klasse in C++ öffentlich abzuleiten, wenn Sie nicht versuchen, etwas Polymorphes zu tun. Die Sprache ist mit kostenlosen Funktionen als Standardfunktion der Sprache ausgestattet, und Sie sollten hier kostenlose Funktionen verwenden.

Stellen Sie sich das so vor - möchten Sie wirklich die Clients Ihres Codes dazu zwingen, eine proprietäre String-Klasse zu verwenden, nur weil Sie einige Methoden anwenden möchten? Im Gegensatz zu Java oder C # (oder ähnlichsten objektorientierten Sprachen) müssen die meisten Benutzer der Basisklasse beim Ableiten einer Klasse in C++ über diese Art von Änderung Bescheid wissen. In Java/C # wird normalerweise auf Klassen über Verweise zugegriffen, die den Zeigern von C++ ähneln. Daher gibt es eine Ebene der Dereferenzierung, die die Clients Ihrer Klasse entkoppelt, sodass Sie eine abgeleitete Klasse ersetzen können, ohne dass andere Clients dies wissen.

In C++ sind Klassen jedoch value-Typen - im Gegensatz zu den meisten anderen OO -Sprachen. Der einfachste Weg, dies zu sehen, ist das, was als das Slicing-Problem bekannt ist. Grundsätzlich gilt:

int StringToNumber(std::string copyMeByValue)
{
    std::istringstream converter(copyMeByValue);
    int result;
    if (converter >> result)
    {
        return result;
    }
    throw std::logic_error("That is not a number.");
}

Wenn Sie dieser Methode einen eigenen String übergeben, wird der Kopierkonstruktor für std::string aufgerufen, um eine Kopie zu erstellen. Nicht der Kopierkonstruktor für Ihr abgeleitetes Objekt - unabhängig davon, welche Kindklasse von std::string übergeben wird. Dies kann zu Inkonsistenzen zwischen Ihren Methoden und den an die Zeichenfolge angefügten Elementen führen. Die Funktion StringToNumber kann das abgeleitete Objekt nicht einfach nehmen und kopieren, weil das abgeleitete Objekt wahrscheinlich eine andere Größe als ein std::string hat. Diese Funktion wurde jedoch so kompiliert, dass nur der Platz für einen std::string im automatischen Speicher reserviert wird. In Java und C # ist dies kein Problem, da beim automatischen Speichern nur Referenztypen beteiligt sind und die Referenzen immer die gleiche Größe haben. Nicht so in C++.

Um es kurz zu machen: Verwenden Sie keine Vererbung für Methoden in C++. Das ist nicht idiomatisch und führt zu Problemen mit der Sprache. Verwenden Sie nach Möglichkeit Funktionen ohne Freunde und Nichtmitglieder, gefolgt von einer Komposition. Verwenden Sie keine Vererbung, es sei denn, Sie sind Metaprogrammierung von Vorlagen oder wollen polymorphes Verhalten. Weitere Informationen finden Sie unter Scott Meyers ' Effective C++ Position 23: Nicht-Freunde-Funktionen für Nicht-Freunde zu Elementfunktionen.

BEARBEITEN: Hier ist ein vollständigeres Beispiel, das das Slicing-Problem zeigt. Sie können die Ausgabe auf codepad.org sehen.

#include <ostream>
#include <iomanip>

struct Base
{
    int aMemberForASize;
    Base() { std::cout << "Constructing a base." << std::endl; }
    Base(const Base&) { std::cout << "Copying a base." << std::endl; }
    ~Base() { std::cout << "Destroying a base." << std::endl; }
};

struct Derived : public Base
{
    int aMemberThatMakesMeBiggerThanBase;
    Derived() { std::cout << "Constructing a derived." << std::endl; }
    Derived(const Derived&) : Base() { std::cout << "Copying a derived." << std::endl; }
    ~Derived() { std::cout << "Destroying a derived." << std::endl; }
};

int SomeThirdPartyMethod(Base /* SomeBase */)
{
    return 42;
}

int main()
{
    Derived derivedObject;
    {
        //Scope to show the copy behavior of copying a derived.
        Derived aCopy(derivedObject);
    }
    SomeThirdPartyMethod(derivedObject);
}
55
Billy ONeal

Um den allgemeinen Ratschlägen die Gegenseite zu bieten (was solide ist, wenn keine besonderen Probleme mit der Ausführlichkeit/Produktivität erkennbar sind) ...

Szenario für eine angemessene Nutzung

Es gibt mindestens ein Szenario, in dem eine öffentliche Ableitung von Basen ohne virtuelle Destruktoren eine gute Entscheidung sein kann:

  • sie möchten einige der Vorteile in Bezug auf die Typensicherheit und die Codelesbarkeit von benutzerdefinierten Typen (Klassen) nutzen.
  • eine vorhandene Basis ist ideal zum Speichern der Daten und ermöglicht Operationen auf niedriger Ebene, die auch vom Client-Code verwendet werden sollen
  • sie möchten die Möglichkeit haben, Funktionen, die diese Basisklasse unterstützen, wiederzuverwenden
  • sie verstehen, dass alle zusätzlichen Invarianten, die Ihre Daten logisch benötigen, nur in Code erzwungen werden können, der explizit als abgeleiteter Typ auf die Daten zugreift. Dies hängt davon ab, inwieweit dies in Ihrem Entwurf "natürlich" vorkommt und wie sehr Sie dem Client vertrauen können Code, um die logisch idealen Invarianten zu verstehen und mit ihnen zusammenzuarbeiten. Möglicherweise möchten Sie, dass Memberfunktionen der abgeleiteten Klasse die Erwartungen erneut überprüfen (und werfen oder was auch immer).
  • die abgeleitete Klasse fügt einige sehr typspezifische Komfortfunktionen hinzu, die über die Daten ausgeführt werden, z. B. benutzerdefinierte Suchen, Datenfilterung/-modifikationen, Streaming, statistische Analyse und (alternative) Iteratoren
  • das Koppeln von Client-Code an die Basis ist geeigneter als das Koppeln an die abgeleitete Klasse (da die Basis entweder stabil ist oder Änderungen an ihr Verbesserungen an der Funktionalität widerspiegeln, die auch für die abgeleitete Klasse von zentraler Bedeutung sind)
    • anders ausgedrückt: Sie möchten, dass die abgeleitete Klasse weiterhin dieselbe API wie die Basisklasse verfügbar macht, auch wenn dies bedeutet, dass der Clientcode geändert werden muss, anstatt ihn auf eine Weise zu isolieren, die das Wachstum der Basis- und abgeleiteten APIs ermöglicht der Synchronisation
  • sie werden keine Zeiger auf Basisobjekte und abgeleitete Objekte in Teilen des Codes mischen, die für das Löschen verantwortlich sind

Dies mag recht restriktiv klingen, aber es gibt viele Fälle in realen Programmen, die diesem Szenario entsprechen.

Hintergrunddiskussion: relative Vorzüge

Beim Programmieren geht es um Kompromisse. Bevor Sie ein konzeptionell "korrektes" Programm schreiben:

  • überlegen Sie, ob zusätzliche Komplexität und Code erforderlich sind, der die eigentliche Programmlogik verschleiert und daher insgesamt fehleranfälliger ist, obwohl ein bestimmtes Problem robuster behandelt wird.
  • die praktischen Kosten gegen die Wahrscheinlichkeit und die Folgen von Problemen abzuwägen und
  • überlegen Sie, was Sie sonst noch mit Ihrer Zeit anfangen könnten.

Wenn die potenziellen Probleme die Verwendung der Objekte betreffen, von denen Sie sich einfach nicht vorstellen können, dass jemand versucht, sie zu nutzen, geben Sie Ihre Einsichten in Zugänglichkeit, Umfang und Art der Verwendung im Programm, oder Sie können Kompilierungsfehler für generieren gefährliche Verwendung (z. B. eine Behauptung, dass die abgeleitete Klassengröße mit der der Basis übereinstimmt, was das Hinzufügen neuer Datenelemente verhindern würde), dann kann alles andere eine vorzeitige Überentwicklung sein. Profitieren Sie von einem übersichtlichen, intuitiven, präzisen Design und Code.

Gründe, Ableitung ohne virtuellen Destruktor in Betracht zu ziehen

Angenommen, Sie haben eine Klasse D, die öffentlich von B abgeleitet ist. Ohne Aufwand sind die Operationen auf B auf D möglich (mit Ausnahme der Konstruktion, aber selbst wenn es viele Konstruktoren gibt, können Sie häufig eine effektive Weiterleitung bereitstellen, indem Sie eine Vorlage für haben jeweils unterschiedliche Anzahl von Konstruktorargumenten: zB template <typename T1, typename T2> D(const T1& x1, const T2& t2) : B(t1, t2) { }. Besser verallgemeinerte Lösung in C++ 0x variadic templates.)

Wenn sich B ändert, macht D diese Änderungen standardmäßig verfügbar - und bleibt dabei synchron -, aber möglicherweise muss jemand die in D eingeführten erweiterten Funktionen überprüfen, um festzustellen, ob sie weiterhin gültig sind. nd die Client-Nutzung.

Umformulieren: Es gibt reduzierte explizite Kopplung zwischen Basis und abgeleiteter Klasse, aber erhöhte Kopplung zwischen Basis und Client.

Dies ist oft NICHT das, was Sie wollen, aber manchmal ist es ideal, und manchmal ist es kein Problem (siehe nächster Absatz). Änderungen an der Basis erzwingen mehr Client-Code-Änderungen an Orten, die über die gesamte Codebasis verteilt sind, und manchmal haben die Benutzer, die die Basis ändern, möglicherweise nicht einmal Zugriff auf den Client-Code, um ihn entsprechend zu überprüfen oder zu aktualisieren. Manchmal ist es jedoch besser: Wenn Sie als Anbieter abgeleiteter Klassen - der "Mann in der Mitte" - möchten, dass Änderungen an der Basisklasse den Clients übermittelt werden, und Sie möchten im Allgemeinen, dass Clients in der Lage sind - manchmal gezwungen -, ihren Code zu aktualisieren, wenn die Wenn sich die Basisklasse ändert, ohne dass Sie ständig involviert sein müssen, ist eine öffentliche Ableitung ideal. Dies ist häufig der Fall, wenn Ihre Klasse nicht so sehr eine eigenständige Entität ist, sondern einen geringen Mehrwert für die Basis darstellt.

In anderen Fällen ist die Basisklassenschnittstelle so stabil, dass die Kopplung als kein Problem angesehen werden kann. Dies gilt insbesondere für Klassen wie Standardcontainer.

Zusammenfassend ist die öffentliche Ableitung eine schnelle Möglichkeit, die ideale, vertraute Basisklassenschnittstelle für die abgeleitete Klasse auf eine Weise zu ermitteln oder anzunähern, die sowohl für den Betreuer als auch für den Client-Codierer präzise und selbstverständlich korrekt ist. welche IMHO - die sich offensichtlich von Sutter, Alexandrescu usw. unterscheidet - die Benutzerfreundlichkeit und Lesbarkeit verbessern und produktivitätssteigernde Tools, einschließlich IDEs, unterstützen kann)

C++ - Codierungsstandards - Sutter & Alexandrescu - Nachteile untersucht

Punkt 35 von C++ Coding Standards listet Probleme mit dem Szenario der Ableitung von std::string auf. In manchen Szenarien ist es gut, dass die Belastung durch das Offenlegen einer großen, aber nützlichen API deutlich wird, aber sowohl gut als auch schlecht, da die Basis-API bemerkenswert stabil ist und Teil der Standardbibliothek ist. Eine stabile Basis ist eine häufige Situation, aber nicht häufiger als eine volatile, und eine gute Analyse sollte sich auf beide Fälle beziehen. Während ich die Themenliste des Buches betrachte, werde ich die Anwendbarkeit der Themen speziell auf die Fälle von etwa kontrastieren:

a) class Issue_Id : public std::string { ...handy stuff... }; <- öffentliche Ableitung, unsere kontroverse Verwendung
b) class Issue_Id : public string_with_virtual_destructor { ...handy stuff... }; <- sicherere OO Ableitung
c) class Issue_Id { public: ...handy stuff... private: std::string id_; }; <- ein kompositorischer Ansatz
d) Verwenden von std::string überall mit freistehenden Supportfunktionen

(Hoffentlich können wir uns darauf einigen, dass die Zusammensetzung eine akzeptable Praxis ist, da sie Einkapselung, Typensicherheit sowie eine potenziell angereicherte API bietet, die über die von std::string hinausgeht.)

Angenommen, Sie schreiben einen neuen Code und beginnen, über die konzeptuellen Entitäten im Sinne von OO nachzudenken. Vielleicht in einem Bug-Tracking-System (ich denke an JIRA), ist einer von ihnen sagen, eine Issue_Id. Der Dateninhalt ist textuell - bestehend aus einer alphabetischen Projekt-ID, einem Bindestrich und einer inkrementellen Ausgabenummer: z. "MYAPP-1234". Issue-IDs können in einem std::string gespeichert werden, und es werden viele fummelige Textsuchen und Manipulationsoperationen für Issue-IDs erforderlich sein - eine große Teilmenge der bereits in std::string bereitgestellten und einige weitere, um die Projekt-ID-Komponente zu erhalten (z. B. Abrufen der Projekt-ID-Komponente) Geben Sie die nächstmögliche Problem-ID an (MYAPP-1235).

Weiter zu Sutters und Alexandrescus Themenliste ...

Nicht-Member-Funktionen funktionieren gut in vorhandenem Code, der bereits strings bearbeitet. Wenn Sie stattdessen ein super_string angeben, erzwingen Sie Änderungen in Ihrer Codebasis, um Typen und Funktionssignaturen in super_string zu ändern.

Der fundamentale Fehler bei dieser Behauptung (und den meisten der folgenden) ist, dass sie die Bequemlichkeit fördert, nur wenige Typen zu verwenden, und die Vorteile der Typensicherheit ignoriert. Es drückt eine Präferenz für d) oben aus, anstatt einen Einblick in c) oder b) als Alternativen zu a). Die Kunst des Programmierens beinhaltet das Abwägen der Vor- und Nachteile verschiedener Typen, um eine angemessene Wiederverwendung, Leistung, Bequemlichkeit und Sicherheit zu erreichen. Die folgenden Absätze erläutern dies.

Bei Verwendung der öffentlichen Ableitung kann der vorhandene Code implizit auf die Basisklasse string als string zugreifen und sich weiterhin so verhalten, wie er es immer war. Es gibt keinen bestimmten Grund zu der Annahme, dass der vorhandene Code zusätzliche Funktionen von super_string verwenden möchte (in unserem Fall Issue_Id). Tatsächlich ist es häufig ein niedrigerer Support-Code, der bereits in der Anwendung vorhanden ist, für die Sie den super_string erstellen. und daher die Bedürfnisse der erweiterten Funktionen nicht berücksichtigen. Angenommen, es gibt eine Nichtmitgliedsfunktion to_upper(std::string&, std::string::size_type from, std::string::size_type to) - diese kann weiterhin auf ein Issue_Id angewendet werden.

Wenn also die Nicht-Mitglieder-Support-Funktion nicht absichtlich bereinigt oder erweitert wird, um sie fest mit dem neuen Code zu verbinden, muss sie nicht berührt werden. Wenn es is überarbeitet wird, um Ausgabe-IDs zu unterstützen (z. B. indem die Einsicht in das Dateninhaltsformat verwendet wird, um nur führende Alpha-Zeichen in Großbuchstaben zu schreiben), ist es wahrscheinlich eine gute Sache, um sicherzustellen, dass es wirklich so ist Issue_Id übergeben, indem eine to_upper(Issue_Id&)-Überladung erstellt und entweder die Ableitungs- oder Kompositionsansätze verwendet werden, die die Typensicherheit gewährleisten. Ob super_string oder Komposition verwendet wird, spielt für Aufwand und Wartbarkeit keine Rolle. Eine to_upper_leading_alpha_only(std::string&) wiederverwendbare, freistehende Support-Funktion ist wahrscheinlich nicht sehr hilfreich - ich kann mich nicht erinnern, wann ich das letzte Mal eine solche Funktion gewünscht habe.

Der Impuls, std::string überall zu verwenden, unterscheidet sich qualitativ nicht von der Annahme all Ihrer Argumente als Container von Varianten oder void*s, sodass Sie Ihre Schnittstellen nicht ändern müssen, um beliebige Daten zu akzeptieren, sondern eine fehleranfällige Implementierung und weniger Selbstdokumentation bewirken Vom Compiler überprüfbarer Code.

Schnittstellenfunktionen, die einen String benötigen, müssen nun: a) die zusätzliche Funktionalität von super_string meiden (unbrauchbar); b) kopiere ihr Argument in einen Superstring (verschwenderisch); oder c) Umwandeln der Zeichenfolgenreferenz in eine Super_String-Referenz (umständlich und möglicherweise unzulässig).

Dies scheint den ersten Punkt zu wiederholen - alten Code, der überarbeitet werden muss, um die neue Funktionalität zu nutzen, obwohl diesmal Client-Code und nicht Support-Code. Wenn die Funktion ihr Argument als Entität behandeln möchte, für die die neuen Operationen relevant sind, beginnt sie sollte damit, ihre Argumente als diesen Typ zu verwenden, und die Clients sollten sie generieren und akzeptieren Sie sie mit diesem Typ. Genau die gleichen Probleme gibt es bei der Komposition. Andernfalls kann c) praktisch und sicher sein, wenn die unten aufgeführten Richtlinien befolgt werden, obwohl sie hässlich sind.

die Member-Funktionen von super_string haben keinen größeren Zugriff auf die Interna von string als die Nicht-Member-Funktionen, da string wahrscheinlich keine geschützten Member hat (denken Sie daran, es war ursprünglich nicht dafür gedacht, von diesen abgeleitet zu werden).

Stimmt, aber manchmal ist das eine gute Sache. Viele Basisklassen haben keine geschützten Daten. Die öffentliche string -Oberfläche ist alles, was zum Bearbeiten des Inhalts erforderlich ist, und nützliche Funktionen (z. B. die oben postulierte get_project_id()) können in diesen Vorgängen elegant ausgedrückt werden. In vielen Fällen, in denen ich mich von Standardcontainern abgeleitet habe, wollte ich deren Funktionalität nicht nach den bestehenden Grundsätzen erweitern oder anpassen - sie sind bereits "perfekte" Container -, sondern ich wollte eine andere Dimension des Verhaltens hinzufügen, die spezifisch ist zu meiner Anwendung und erfordert keinen privaten Zugriff. Weil sie bereits gute Container sind, können sie gut wiederverwendet werden.

Wenn super_string einige der Funktionen von string verbirgt (und die Neudefinition einer nicht-virtuellen Funktion in einer abgeleiteten Klasse nicht überschreibt, wird sie nur ausgeblendet), kann dies zu weit verbreiteter Verwirrung in Code führen, der die automatisch konvertierten Funktionen von string manipuliert von super_strings.

Gilt auch für die Komposition - und dies ist wahrscheinlicher, da der Code nicht standardmäßig die Dinge durchläuft und damit synchron bleibt. In einigen Situationen gilt dies auch für polymorphe Hierarchien zur Laufzeit. Getaufte benannte Funktionen, die sich in Klassen, die anfänglich als austauschbar erscheinen, anders verhalten - nur böse. Dies ist praktisch die übliche Vorsicht bei der korrekten OO Programmierung und wiederum kein ausreichender Grund, die Vorteile der Typensicherheit usw. aufzugeben.

Was ist, wenn super_string von string erben möchte, um weitere hinzuzufügen state [Erklärung zum Schneiden]

Einverstanden - keine gute Situation, und irgendwo neige ich persönlich dazu, die Grenze zu ziehen, da es oft die Probleme des Löschens durch einen Zeiger auf die Basis vom Bereich der Theorie zum sehr praktischen Bereich verschiebt - Destruktoren werden für zusätzliche Mitglieder nicht aufgerufen. Trotzdem kann Slicing oft das tun, was gewünscht wird - angesichts des Ansatzes, super_string abzuleiten, nicht seine vererbte Funktionalität zu ändern, sondern eine weitere "Dimension" anwendungsspezifischer Funktionalität hinzuzufügen ....

Es ist zwar mühsam, Passthrough-Funktionen für die Member-Funktionen zu schreiben, die Sie beibehalten möchten, aber eine solche Implementierung ist weitaus besser und sicherer als die Verwendung von öffentlicher oder nicht öffentlicher Vererbung.

Na ja, sicherlich einverstanden über die Langeweile ....

Richtlinien für eine erfolgreiche Ableitung ohne virtuellen Destruktor

  • vermeiden Sie im Idealfall das Hinzufügen von Datenelementen in abgeleiteten Klassen: Varianten des Aufteilens können Datenelemente versehentlich entfernen, beschädigen oder nicht initialisieren ...
  • noch mehr: Vermeiden Sie Nicht-POD-Datenmember: Das Löschen über Basisklassenzeiger ist ohnehin ein technisch undefiniertes Verhalten, aber wenn Nicht-POD-Typen ihre Destruktoren nicht ausführen, ist es wahrscheinlicher, dass nicht theoretische Probleme mit Ressourcenlecks und schlechten Referenzzahlen auftreten usw.
  • ehre den Liskov Substitution Principal/du kannst neue Invarianten nicht robust pflegen
    • wenn Sie beispielsweise von std::string ableiten, können Sie einige Funktionen nicht abfangen und erwarten, dass Ihre Objekte in Großbuchstaben geschrieben bleiben. Jeder Code, der über std::string& oder ...* auf sie zugreift, kann die ursprünglichen Funktionsimplementierungen von std::string verwenden, um den Wert zu ändern.
    • leiten Sie ab, eine übergeordnete Entität in Ihrer Anwendung zu modellieren, um die vererbte Funktionalität um einige Funktionen zu erweitern, die die Basis nutzen, aber nicht in Konflikt stehen. Erwarten oder versuchen Sie nicht, die vom Basistyp gewährten Basisoperationen und den Zugriff auf diese Operationen zu ändern
  • beachten Sie die Kopplung: Die Basisklasse kann nicht entfernt werden, ohne den Client-Code zu beeinflussen, selbst wenn die Basisklasse eine unangemessene Funktionalität aufweist. Die Verwendbarkeit Ihrer abgeleiteten Klasse hängt von der fortlaufenden Eignung der Basis ab
    • selbst wenn Sie Composition verwenden, müssen Sie das Datenelement manchmal aufgrund von Performance-, Thread-Sicherheitsproblemen oder fehlender Wertesemantik verfügbar machen - damit der Verlust der Kapselung durch öffentliche Ableitungen nicht spürbar schlimmer wird
  • je wahrscheinlicher die Leute, die die potenziell abgeleitete Klasse verwenden, sich ihrer Implementierungskompromisse nicht bewusst sind, desto weniger können Sie es sich leisten, sie gefährlich zu machen
    • daher sollten weitverbreitete Bibliotheken auf niedriger Ebene mit vielen gelegentlichen Ad-hoc-Benutzern vor gefährlichen Ableitungen gewarnt sein, als wenn Programmierer die Funktionalität auf Anwendungsebene und/oder in "privaten" Implementierungen/Bibliotheken routinemäßig verwenden

Zusammenfassung

Eine solche Herleitung ist nicht unproblematisch. Denken Sie also nicht darüber nach, es sei denn, das Endergebnis rechtfertigt die Mittel. Trotzdem lehne ich jede Behauptung ab, dass dies in bestimmten Fällen nicht sicher und angemessen verwendet werden kann - es ist nur eine Frage, wo die Grenze gezogen werden muss.

Persönliche Erfahrung

Ich stamme manchmal von std::map<>, std::vector<>, std::string usw. - Ich bin noch nie von Problemen mit dem Aufteilen oder Löschen über Basisklassenzeiger verbrannt worden, und ich habe viel Zeit und Energie für wichtigere Dinge gespart. Ich lagere solche Objekte nicht in heterogenen polymorphen Behältern. Sie müssen jedoch prüfen, ob alle Programmierer, die das Objekt verwenden, die Probleme kennen und wahrscheinlich entsprechend programmieren. Ich persönlich schreibe meinen Code gerne, um Heap- und Laufzeit-Polymorphismus nur bei Bedarf zu verwenden, während einige Leute (aufgrund von Java -Hintergründen, ihres bevorzugten Ansatzes zum Verwalten von Kompilierungsabhängigkeiten oder zum Wechseln zwischen Laufzeitverhalten, Testeinrichtungen usw.) Verwenden Sie sie gewohnheitsmäßig und müssen Sie sich daher mehr Gedanken über sichere Operationen über Basisklassenzeiger machen.

24
Tony Delroy

Der Destruktor ist nicht nur nicht virtuell, std :: string enthält keine virtuellen Funktionen überhaupt und keine geschützten Member. Das macht es sehr schwer für die abgeleitete Klasse, ihre Funktionalität zu ändern.

Warum sollten Sie dann davon ableiten?

Ein weiteres Problem bei der Nicht-Polymorphie besteht darin, dass bei der Übergabe der abgeleiteten Klasse an eine Funktion, die einen String-Parameter erwartet, die zusätzliche Funktionalität nur abgeschnitten wird und das Objekt wieder als reiner String angezeigt wird.

10
Bo Persson

Wenn Sie wirklich daraus ableiten möchten (nicht diskutieren, warum Sie dies tun möchten), denke ich, können Sie die Derived-Klasse direkter Heap-Instantiierung verhindern, indem Sie operator new als privat festlegen:

class StringDerived : public std::string {
//...
private:
  static void* operator new(size_t size);
  static void operator delete(void *ptr);
}; 

Auf diese Weise beschränken Sie sich jedoch auf alle dynamischen StringDerived-Objekte.

9
beduin

Warum sollte man nicht von der C++ - std-String-Klasse ableiten?

Weil es nicht notwendig ist . Wenn Sie DerivedString für die Funktionserweiterung verwenden möchten; Ich sehe kein Problem beim Ableiten von std::string. Die einzige Sache ist, dass Sie nicht zwischen beiden Klassen interagieren sollten (d. H. Nicht string als Empfänger für DerivedString verwenden).

Gibt es eine Möglichkeit, den Client daran zu hindern, Base* p = new Derived() zu tun?

Ja . Stellen Sie sicher, dass Sie inline-Wrapper für Base-Methoden in der Derived-Klasse bereitstellen. z.B.

class Derived : protected Base { // 'protected' to avoid Base* p = new Derived
  const char* c_str () const { return Base::c_str(); }
//...
};
4
iammilind

Es gibt zwei einfache Gründe, um nicht von einer nicht-polymorphen Klasse abgeleitet zu werden:

  • Technical: Slicing Bugs (weil in C++ wir den Wert übergeben, sofern nicht anders angegeben)
  • Functional: Wenn es nicht polymorph ist, können Sie den gleichen Effekt mit der Komposition und einigen Funktionsweiterleitungen erzielen

Wenn Sie std::string neue Funktionen hinzufügen möchten, sollten Sie zunächst die Verwendung freier Funktionen (möglicherweise Vorlagen) in Betracht ziehen, wie dies die Bibliothek Boost String Algorithm tut.

Wenn Sie neue Datenelemente hinzufügen möchten, schließen Sie den Klassenzugriff ordnungsgemäß ein, indem Sie ihn (Zusammensetzung) in eine Klasse Ihres eigenen Designs einbetten.

EDIT:

@ Tony bemerkte zu Recht, dass der Functional - Grund, den ich zitierte, wahrscheinlich für die meisten Leute bedeutungslos war. Es gibt eine einfache Faustregel in gutem Design, die besagt, dass, wenn Sie eine Lösung aus mehreren auswählen können, die Lösung mit der schwächeren Kopplung in Betracht gezogen werden sollte. Zusammensetzung hat eine schwächere Kopplung als Vererbung und sollte daher bevorzugt werden, wenn dies möglich ist.

Außerdem gibt Ihnen die Komposition die Möglichkeit, die Klassenmethode des Originals hübsch einzuwickeln. Dies ist nicht möglich, wenn Sie die Vererbung (öffentlich) auswählen und die Methoden nicht virtuell sind (was hier der Fall ist).

2
Matthieu M.

Wenn Sie der abgeleiteten Klasse std :: string ein Element (eine Variable) hinzufügen, werden Sie den Stapel systematisch verschrauben, wenn Sie versuchen, die std-Goodies mit einer Instanz der abgeleiteten Klasse std :: string zu verwenden. Da die Stack-Zeiger [der Indizes] der stdc ++ -Funktionen/-Mitglieder auf die Größe/Begrenzung der Instanzgröße (base std :: string) [festgelegt] und [angepasst] sind. 

Recht?

Bitte korrigieren Sie mich, wenn ich falsch liege.

0
Bretzelus

Der C++ - Standard besagt, dass, wenn der Destruktor der Basisklasse nicht virtuell ist und Sie ein Objekt der Basisklasse löschen, das auf das Objekt einer abgeleiteten Klasse verweist, ein undefiniertes Verhalten verursacht wird.

C++ - Standardabschnitt 5.3.5/3:

wenn sich der statische Typ des Operanden von seinem dynamischen Typ unterscheidet, muss der statische Typ eine Basisklasse des dynamischen Typs des Operanden sein und der statische Typ muss einen virtuellen Destruktor haben, oder das Verhalten ist undefiniert.

Um klar zu sein über die nicht-polymorphe Klasse und den Bedarf an virtuellem Destruktor
Ein virtueller Destruktor soll das polymorphe Löschen von Objekten durch delete-expression erleichtern. Wenn keine polymorphen Objekte gelöscht werden, benötigen Sie keinen virtuellen Destruktor.

Warum nicht von String Class ableiten?
Es sollte generell vermieden werden, von einer Standard-Containerklasse abgeleitet zu werden, gerade weil sie keine virtuellen Destruktoren haben, die das polymorphe Löschen von Objekten unmöglich machen.
Was die String-Klasse angeht, hat die String-Klasse keine virtuellen Funktionen. Es gibt also nichts, was Sie möglicherweise überschreiben können. Das Beste, was Sie tun können, ist etwas zu verstecken.

Wenn Sie überhaupt eine string-ähnliche Funktionalität haben möchten, sollten Sie eine eigene Klasse schreiben und nicht von std :: string erben.

0
Alok Save