
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.
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.
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.

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.

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?

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) { ... }
9931500cookie-checkStandardcontainer untergliedern/erben?yes
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