Standardcontainer untergliedern/erben?

Lesezeit: 12 Minuten

Standardcontainer untergliedernerben
iammilind

Ich lese diese Aussagen oft auf Stack Overflow. Ich persönlich finde kein Problem damit, es sei denn, ich verwende es auf polymorphe Weise; dh wo ich verwenden muss virtual Zerstörer.

Wenn ich die Funktionalität eines Standardcontainers erweitern/hinzufügen möchte, was ist dann ein besserer Weg, als einen zu erben? Das Einschließen dieser Container in eine benutzerdefinierte Klasse erfordert viel mehr Aufwand und ist immer noch unsauber.

  • Ich würde sagen, es hängt davon ab, welche Art von Erweiterung Sie hinzufügen möchten.

    – NirMH

    24. Juli 2011 um 10:10 Uhr

  • Ich weiß nicht, ob diese ältere Frage von mir ein Duplikat ist, aber sicher verwandt 🙂 stackoverflow.com/questions/4353203/…

    – Armen Tsirunyan

    24. Juli 2011 um 10:30 Uhr

Es gibt eine Reihe von Gründen, warum dies eine schlechte Idee ist.

Erstens ist dies eine schlechte Idee, weil die Standardcontainer haben keine virtuellen Destruktoren. Sie sollten niemals etwas polymorph verwenden, das keine virtuellen Destruktoren hat, da Sie die Bereinigung in Ihrer abgeleiteten Klasse nicht garantieren können.

Grundregeln für virtuelle dtors

Zweitens ist es wirklich schlechtes Design. Und es gibt tatsächlich mehrere Gründe, warum es schlechtes Design ist. Erstens sollten Sie die Funktionalität von Standard-Containern immer durch generisch arbeitende Algorithmen erweitern. Dies ist ein einfacher Komplexitätsgrund – wenn Sie einen Algorithmus für jeden Container schreiben müssen, für den er gilt, und Sie M Container und N Algorithmen haben, müssen Sie M x N Methoden schreiben. Wenn Sie Ihre Algorithmen generisch schreiben, haben Sie nur N Algorithmen. Sie erhalten also viel mehr Wiederverwendung.

Es ist auch ein wirklich schlechtes Design, weil Sie eine gute Kapselung brechen, indem Sie vom Container erben. Eine gute Faustregel lautet: Wenn Sie mit der öffentlichen Schnittstelle eines Typs ausführen können, was Sie benötigen, machen Sie dieses neue Verhalten außerhalb des Typs. Dies verbessert die Einkapselung. Wenn Sie ein neues Verhalten implementieren möchten, machen Sie es zu einer Namespace-Bereichsfunktion (wie die Algorithmen). Wenn Sie eine neue Invariante auferlegen müssen, verwenden Sie Containment in einer Klasse.

Eine klassische Beschreibung der Kapselung

Schließlich sollten Sie Vererbung im Allgemeinen nie als Mittel zur Erweiterung des Verhaltens einer Klasse betrachten. Dies ist einer der große, schlechte Lügen der frühen OOP-Theorie das entstand aus unklarem Nachdenken über die Wiederverwendung und wird bis heute gelehrt und gefördert, obwohl es eine klare Theorie gibt, warum es schlecht ist. Wenn Sie die Vererbung verwenden, um das Verhalten zu erweitern, binden Sie dieses erweiterte Verhalten so an Ihren Schnittstellenvertrag, dass die Benutzer an zukünftige Änderungen gebunden sind. Angenommen, Sie haben eine Klasse vom Typ Socket, die über das TCP-Protokoll kommuniziert, und Sie erweitern ihr Verhalten, indem Sie eine Klasse SSLSocket von Socket ableiten und das Verhalten des höheren SSL-Stack-Protokolls auf Socket implementieren. Angenommen, Sie erhalten eine neue Anforderung, dasselbe Kommunikationsprotokoll zu verwenden, jedoch über eine USB-Leitung oder über Telefonie. Sie müssten all diese Arbeit ausschneiden und in eine neue Klasse einfügen, die von einer USB-Klasse oder einer Telefonie-Klasse abgeleitet ist. Und wenn Sie jetzt einen Fehler finden, müssen Sie ihn an allen drei Stellen beheben, was nicht immer passieren wird, was bedeutet, dass Fehler länger dauern und nicht immer behoben werden …

Dies gilt allgemein für jede Vererbungshierarchie A->B->C->… Wenn Sie die Verhaltensweisen, die Sie in abgeleiteten Klassen wie B, C, … erweitert haben, auf Objekte anwenden möchten, die nicht der Basisklasse A angehören, Sie müssen neu entwerfen oder die Implementierung duplizieren. Dies führt zu sehr monolithischen Designs, die später sehr schwer zu ändern sind (denken Sie an Microsofts MFC oder ihr .NET oder – nun, sie machen diesen Fehler häufig). Stattdessen sollten Sie fast immer an eine Erweiterung durch Komposition denken, wann immer dies möglich ist. Vererbung sollte verwendet werden, wenn Sie an das “Offene / Geschlossene Prinzip” denken. Sie sollten abstrakte Basisklassen und dynamische Polymorphismus-Laufzeit durch geerbte Klasse haben, jede wird vollständige Implementierungen haben. Hierarchien sollten nicht tief sein – fast immer zwei Ebenen. Verwenden Sie nur dann mehr als zwei, wenn Sie unterschiedliche dynamische Kategorien haben, die zu einer Vielzahl von Funktionen gehören, die diese Unterscheidung für die Typsicherheit benötigen. Verwenden Sie in diesen Fällen abstrakte Basen bis zu den Blattklassen, die die Implementierung haben.

  • +1 : Komposition > Vererbung

    – Matthias

    2. September 2011 um 15:30 Uhr

  • Sehr guter Punkt zum Thema Erbschaft.

    – Alexander C.

    9. Februar 2012 um 21:44 Uhr

  • ++++++++++++1. Die Teile, die ich sehr mag: 1) Generisches & orthogonales Design; 2) Wenn Dinge über eine öffentliche Schnittstelle erledigt werden können, halten Sie diese extern; 3) Socket/SSL/USB-Beispiel – Ich habe viel Legacy-Code gesehen, der in diese Falle getappt ist

    – Vietnam

    3. Dezember 2012 um 23:47 Uhr


  • Wenn ein Fehler Probleme in allen USB-, Telefonie- und SSL-Klassen verursachte, würde das nicht bedeuten, dass es sich tatsächlich um gemeinsam genutzten Code handelte, der Teil einer gemeinsamen Basis hätte sein sollen (und nur an einer Stelle behoben werden sollte)?

    – Jarvisteve

    2. Dezember 2014 um 19:51 Uhr

  • @jarvisteve: Es gibt keine SSL-Klasse. es gibt SSLSocket, SSLUSB, … Alle haben einen Fehler im Zusammenhang mit der kopierten SSL-Implementierung. Es gibt keine gemeinsame Basisklasse und Sie können keine haben. Und ja, der springende Punkt bei der Behebung besteht darin, den Code an einer Stelle zu haben.

    – Karoly Horvath

    3. Dezember 2014 um 18:07 Uhr

Vielleicht wird vielen Leuten hier diese Antwort nicht gefallen, aber es ist an der Zeit, dass etwas Ketzerei erzählt wird und ja … auch gesagt wird, dass “der König nackt ist!”

Alle Motivationen gegen die Herleitung sind schwach. Die Ableitung unterscheidet sich nicht von der Zusammensetzung. Es ist nur eine Möglichkeit, “Dinge zusammenzufügen”. Komposition fügt Dinge zusammen und gibt ihnen Namen, Vererbung tut dies, ohne explizite Namen zu geben.

Wenn Sie einen Vektor benötigen, der die gleiche Schnittstelle und Implementierung von std::vect plus etwas mehr hat, können Sie:

Verwenden Sie die Komposition und schreiben Sie alle eingebetteten Objektfunktionsprototypen neu, die die Funktion implementieren, die sie delegiert (und wenn sie 10000 sind … ja: seien Sie bereit, all diese 10000 neu zu schreiben) oder …

erben Sie es und fügen Sie genau das hinzu, was Sie brauchen (und … schreiben Sie einfach Konstruktoren um, bis C++-Anwälte entscheiden, sie auch vererbbar zu machen: Ich erinnere mich noch an die eifernde Diskussion vor 10 Jahren darüber, “warum Ctors sich nicht gegenseitig anrufen können” und warum es so ist ist eine “schlechte, schlechte, schlechte Sache” … bis C ++ 11 es erlaubte und plötzlich all diese Eiferer die Klappe hielten!) und den neuen Destruktor nicht virtuell ließen, da er der ursprüngliche war.

Genau wie für jede Klasse, die hat etwas virtuelle Methode u etwas nicht, Sie wissen, dass Sie nicht vorgeben können, die nicht virtuelle Methode von abgeleitet aufzurufen, indem Sie die Basis adressieren, das gleiche gilt für löschen. Es gibt keinen Grund für delete, besondere Sorgfalt vorzutäuschen. Ein Programmierer, der weiß, dass das, was nicht virtuell ist, nicht aufrufbar ist und die Basis adressiert, weiß auch, dass er nach dem Zuweisen Ihrer abgeleiteten Basis kein Löschen auf Ihrer Basis verwenden darf.

All das „vermeide dies“ „tu das nicht“ klingt immer wie „Moralisierung“ von etwas, das von Natur aus agnostisch ist. Alle Merkmale einer Sprache existieren, um ein Problem zu lösen. Die Tatsache, ob ein bestimmter Weg zur Lösung des Problems gut oder schlecht ist, hängt vom Kontext ab, nicht von der Funktion selbst. Wenn das, was Sie tun, viele Container bedienen muss, ist Vererbung das Richtige wahrscheinlich nicht der Weg (Sie müssen für alle wiederholen). Wenn es sich um einen bestimmten Fall handelt … Vererbung ist eine Art zu komponieren. Vergessen Sie OOP-Purismen: C++ ist kein “reines OOP” und Container sind überhaupt keine OOP.

  • Sie sollten keine Zusammensetzung verwenden, es sei denn, Sie müssen eine neue Invariante erzwingen. Aber dann müssen Sie all diese Schnittstellenmethoden sowieso überschreiben (da Sie eine neue Invariante erzwingen müssen!). Wenn Sie keine neue Invariante haben, ist eine Namespace-Scope-freie Funktion völlig angemessen und funktioniert viel besser als Vererbung, da sie generisch auf mehrere Typen angewendet werden kann. Dies ist eine objektive Wiederverwendung, keine Meinung. Ich kenne niemanden, der gesagt hat, Ctors anzurufen sei schlecht – es könnte nicht in der Sprache gemacht werden, die völlig anders ist. Nichts von dieser Moral, es sind objektive Metriken der Wiederverwendung.

    – ex0du5

    12. September 2011 um 22:32 Uhr

  • Ich bin nicht einverstanden. OOP-Vererbung und OOP-Komposition sind ein anderes Konzept als C++-Vererbung, C++-Member. Ich kann die Objektvererbung mit C++-Mitgliedern implementieren, da ich die Objektzusammensetzung mit der C++-Vererbung implementieren kann. Die Tatsache, dass die C++-Vererbung hauptsächlich zur Implementierung der Objektvererbung und C++-Member hauptsächlich zur Implementierung der Objektkomposition verwendet werden, ist aufgrund der Sprachgeschichte nur eine gängige Konvention. Die Wiederverwendung von Objekten ist ein Konzept, das eng mit einer OOP-Abstraktion zusammenhängt. Die Wiederverwendung von Code ist ein Konzept, das eng mit der “Eingabe” von Sprachen zusammenhängt. Die beiden müssen nicht notwendigerweise gleich sein.

    – Emilio Garavaglia

    13. September 2011 um 7:27 Uhr

  • Ich stimme hier ex0du5 zu … Ich würde sagen, dass die Ableitung nicht schlecht ist, aber dass es fast immer bessere Lösungen gibt. Insbesondere wenn Sie eine implementieren möchten std::vector plus “einige weitere Funktionen”, eine bessere Lösung besteht darin, diese Funktionen freistehend zu machen, möglicherweise mit Vorlagen. Das funktioniert besser als Komposition oder Vererbung. Falls Sie Datenelemente hinzufügen, werden Sie sehr wahrscheinlich einige Invarianten hinzufügen, in diesem Fall müssen Sie die Schnittstelle von Vektor ändern, um sie zu erzwingen …

    – Jakow Galka

    3. Mai 2013 um 17:33 Uhr

  • @ybungalobill: Hast du jemals versucht, beide Wege zu gehen? Mein Verdacht ist, dass Sie genau das sagen, was Ihnen gesagt wurde und was Sie nie getan haben. Freistehende Funktionen sind in Ordnung, bis Sie den “Zustand” nicht zwischen ihnen teilen müssen (dafür sind Klassen schließlich da).

    – Emilio Garavaglia

    4. Mai 2013 um 14:11 Uhr

  • +1 für die Schüssel mit frischer Luft. Das einzige wirkliche Argument dagegen ist, dass die std lib absichtlich kaputt ist.

    Benutzer948581

    12. September 2014 um 11:19 Uhr

Standardcontainer untergliedernerben
ErsatzHermelin

Das öffentliche Vererben ist aus allen Gründen, die andere angegeben haben, ein Problem, nämlich dass Ihr Container auf die Basisklasse hochgestuft werden kann, die keinen virtuellen Destruktor oder virtuellen Zuweisungsoperator hat, was zu Slicing-Problemen führen kann.

Privat zu erben ist dagegen weniger ein Thema. Betrachten Sie das folgende Beispiel:

#include <vector>
#include <iostream>

// private inheritance, nobody else knows about the inheritance, so nobody is upcasting my
// container to a std::vector
template <class T> class MyVector : private std::vector<T>
{
private:
    // in case I changed to boost or something later, I don't have to update everything below
    typedef std::vector<T> base_vector;

public:
    typedef typename base_vector::size_type       size_type;
    typedef typename base_vector::iterator        iterator;
    typedef typename base_vector::const_iterator  const_iterator;

    using base_vector::operator[];

    using base_vector::begin;
    using base_vector::clear;
    using base_vector::end;
    using base_vector::erase;
    using base_vector::push_back;
    using base_vector::reserve;
    using base_vector::resize;
    using base_vector::size;

    // custom extension
    void reverse()
    {
        std::reverse(this->begin(), this->end());
    }
    void print_to_console()
    {
        for (auto it = this->begin(); it != this->end(); ++it)
        {
            std::cout << *it << '\n';
        }
    }
};


int main(int argc, char** argv)
{
    MyVector<int> intArray;
    intArray.resize(10);
    for (int i = 0; i < 10; ++i)
    {
        intArray[i] = i + 1;
    }
    intArray.print_to_console();
    intArray.reverse();
    intArray.print_to_console();

    for (auto it = intArray.begin(); it != intArray.end();)
    {
        it = intArray.erase(it);
    }
    intArray.print_to_console();

    return 0;
}

AUSGANG:

1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1

Sauber und einfach und gibt Ihnen die Freiheit, Standardcontainer ohne großen Aufwand zu erweitern.

Und wenn Sie daran denken, etwas Dummes zu tun, so:

std::vector<int>* stdVector = &intArray;

Du bekommst das:

error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible

Sie sollten davon absehen, öffentlich von Standard-Containern abzuleiten. Sie können wählen zwischen privates Erbe und Komposition und es scheint mir, dass alle allgemeinen Richtlinien darauf hindeuten, dass die Komposition hier besser ist, da Sie keine Funktion außer Kraft setzen. Leiten Sie STL-Container nicht öffentlich ab – es ist wirklich nicht nötig.

Übrigens, wenn Sie dem Container eine Reihe von Algorithmen hinzufügen möchten, sollten Sie erwägen, sie als freistehende Funktionen hinzuzufügen, die einen Iteratorbereich einnehmen.

Das Problem ist, dass Sie oder jemand anderes versehentlich Ihre erweiterte Klasse an eine Funktion übergeben, die einen Verweis auf die Basisklasse erwartet. Das wird effektiv (und lautlos!) die Erweiterungen abschneiden und einige schwer zu findende Fehler erzeugen.

Einige Weiterleitungsfunktionen schreiben zu müssen, scheint im Vergleich ein kleiner Preis zu sein.

  • Wie wäre es mit private Nachlass ?

    – iammilind

    24. Juli 2011 um 10:42 Uhr

  • @iammilind: Wenn Sie privat erben, können Sie die geerbte Klasse nicht an diese Funktion übergeben. Auf die Basisklasse kann nicht zugegriffen werden. – Eine privat vererbte Klasse ist nicht die Basisklasse, und die Vererbung ist nur ein Implementierungsdetail.

    – Onkel Bens

    24. Juli 2011 um 11:13 Uhr


  • Gute Antwort! Warum wird Slicing nicht erwähnt, wenn jemand nach der Vererbung von Werttypen fragt?

    – Ich bin echt

    21. Januar 2016 um 18:25 Uhr

1647073820 772 Standardcontainer untergliedernerben
Hündchen

Denn Sie können nie garantieren, dass Sie sie nicht polymorph verwendet haben. Sie betteln um Probleme. Es ist keine große Sache, sich die Mühe zu machen, ein paar Funktionen zu schreiben, und selbst das zu wollen, ist bestenfalls zweifelhaft. Was ist mit der Kapselung passiert?

  • Wie wäre es mit private Nachlass ?

    – iammilind

    24. Juli 2011 um 10:42 Uhr

  • @iammilind: Wenn Sie privat erben, können Sie die geerbte Klasse nicht an diese Funktion übergeben. Auf die Basisklasse kann nicht zugegriffen werden. – Eine privat vererbte Klasse ist nicht die Basisklasse, und die Vererbung ist nur ein Implementierungsdetail.

    – Onkel Bens

    24. Juli 2011 um 11:13 Uhr


  • Gute Antwort! Warum wird Slicing nicht erwähnt, wenn jemand nach der Vererbung von Werttypen fragt?

    – Ich bin echt

    21. Januar 2016 um 18:25 Uhr

1647073821 269 Standardcontainer untergliedernerben
tp1

Der häufigste Grund, von den Containern erben zu wollen, ist, dass Sie der Klasse eine Elementfunktion hinzufügen möchten. Da stdlib selbst nicht änderbar ist, wird Vererbung als Ersatz angesehen. Dies funktioniert jedoch nicht. Es ist besser, eine freie Funktion zu machen, die einen Vektor als Parameter nimmt:

void f(std::vector<int> &v) { ... }

993150cookie-checkStandardcontainer untergliedern/erben?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy