Ich habe eine Klasse, die std :: vector anpasst, um einen Container mit domänenspezifischen Objekten zu modellieren. Ich möchte dem Benutzer den größten Teil der std :: vector-API zur Verfügung stellen, damit er vertraute Methoden (Größe, Löschen, At usw.) und Standardalgorithmen für den Container verwenden kann. Dies scheint für mich ein wiederkehrendes Muster in meinen Entwürfen zu sein:
class MyContainer : public std::vector<MyObject>
{
public:
// Redeclare all container traits: value_type, iterator, etc...
// Domain-specific constructors
// (more useful to the user than std::vector ones...)
// Add a few domain-specific helper methods...
// Perhaps modify or hide a few methods (domain-related)
};
Ich bin mir der Praxis bewusst, Komposition gegenüber Vererbung zu bevorzugen, wenn eine Klasse für die Implementierung wiederverwendet wird - aber es muss eine Grenze geben! Wenn ich alles an std :: vector delegieren würde, gäbe es (nach meiner Zählung) 32 Weiterleitungsfunktionen!
Meine Fragen sind also ... Ist es in solchen Fällen wirklich so schlimm, die Implementierung zu erben? Was sind die Risiken? Gibt es eine sicherere Möglichkeit, dies zu implementieren, ohne so viel zu tippen? Bin ich ein Ketzer für die Verwendung der Implementierungsvererbung? :) :)
Bearbeiten:
Wie wäre es damit, klar zu machen, dass der Benutzer MyContainer nicht über einen std :: vector <> -Zeiger verwenden sollte:
// non_api_header_file.h
namespace detail
{
typedef std::vector<MyObject> MyObjectBase;
}
// api_header_file.h
class MyContainer : public detail::MyObjectBase
{
// ...
};
Die Boost-Bibliotheken scheinen dies die ganze Zeit zu tun.
Bearbeiten 2:
Einer der Vorschläge war die Verwendung freier Funktionen. Ich werde es hier als Pseudocode zeigen:
typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...
Eine OO-Methode:
typedef std::vector<MyObject> MyCollection;
class MyCollectionWrapper
{
public:
// Constructor
MyCollectionWrapper(arguments...) {construct coll_}
// Access collection directly
MyCollection& collection() {return coll_;}
const MyCollection& collection() const {return coll_;}
// Special domain-related methods
result mySpecialMethod(arguments...);
private:
MyCollection coll_;
// Other domain-specific member variables used
// in conjunction with the collection.
}
Antworten:
Das Risiko besteht in der Freigabe durch einen Zeiger auf die Basisklasse ( Löschen , Löschen [] und möglicherweise andere Freigabemethoden). Da diese Klassen ( deque , map , string usw.) keine virtuellen dtors haben, ist es unmöglich, sie mit nur einem Zeiger auf diese Klassen ordnungsgemäß zu bereinigen:
struct BadExample : vector<int> {}; int main() { vector<int>* p = new BadExample(); delete p; // this is Undefined Behavior return 0; }
Das heißt, wenn Sie bereit sind, sicherzustellen, dass Sie dies niemals versehentlich tun, hat es wenig großen Nachteil, sie zu erben - aber in einigen Fällen ist das ein großes Wenn. Weitere Nachteile sind Konflikte mit Implementierungsspezifikationen und -erweiterungen (von denen einige möglicherweise keine reservierten Bezeichner verwenden) und der Umgang mit aufgeblähten Schnittstellen ( insbesondere Zeichenfolge ). In einigen Fällen ist jedoch eine Vererbung vorgesehen, da Containeradapter wie der Stapel ein geschütztes Element c (den zugrunde liegenden Container, den sie anpassen) haben und auf den fast nur von einer abgeleiteten Klasseninstanz aus zugegriffen werden kann.
Anstelle von Vererbung oder Komposition sollten Sie freie Funktionen schreiben , die entweder ein Iteratorpaar oder eine Containerreferenz verwenden, und diese bearbeiten. Praktisch der gesamte <Algorithmus> ist ein Beispiel dafür. und make_heap , pop_heap und push_heap , insbesondere, ist ein Beispiel von freien Funktionen anstelle einen domänenspezifischen Behälter verwenden.
Verwenden Sie also die Containerklassen für Ihre Datentypen und rufen Sie dennoch die freien Funktionen für Ihre domänenspezifische Logik auf. Mit einem typedef können Sie dennoch eine gewisse Modularität erreichen, wodurch Sie sowohl das Deklarieren vereinfachen als auch einen einzelnen Punkt bereitstellen können, wenn ein Teil von ihnen geändert werden muss:
typedef std::deque<int, MyAllocator> Example; // ... Example c (42); example_algorithm(c); example_algorithm2(c.begin() + 5, c.end() - 5); Example::iterator i; // nested types are especially easier
Beachten Sie, dass sich value_type und allocator ändern können, ohne den späteren Code mithilfe von typedef zu beeinflussen, und dass sich sogar der Container von einer Deque zu einem Vektor ändern kann .
quelle
Sie können die private Vererbung und das Schlüsselwort "using" kombinieren, um die meisten der oben genannten Probleme zu umgehen: Die private Vererbung ist "in Bezug auf implementiert", und da sie privat ist, können Sie keinen Zeiger auf die Basisklasse halten
#include <string> #include <iostream> class MyString : private std::string { public: MyString(std::string s) : std::string(s) {} using std::string::size; std::string fooMe(){ return std::string("Foo: ") + *this; } }; int main() { MyString s("Hi"); std::cout << "MyString.size(): " << s.size() << std::endl; std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl; }
quelle
private
Vererbung immer noch Vererbung ist und somit eine stärkere Beziehung als Komposition. Dies bedeutet insbesondere, dass eine Änderung der Implementierung Ihrer Klasse zwangsläufig die Binärkompatibilität beeinträchtigt.Wie bereits erwähnt, verfügen STL-Container nicht über virtuelle Destruktoren, sodass das Erben von ihnen bestenfalls unsicher ist. Ich habe generische Programmierung mit Vorlagen immer als einen anderen OO-Stil betrachtet - einen ohne Vererbung. Die Algorithmen definieren die Schnittstelle, die sie benötigen. Es ist so nah wie möglich an Duck Typing in einer statischen Sprache.
Jedenfalls habe ich der Diskussion etwas hinzuzufügen. Die Art und Weise, wie ich zuvor meine eigenen Vorlagenspezialisierungen erstellt habe, besteht darin, Klassen wie die folgenden zu definieren, die als Basisklassen verwendet werden sollen.
template <typename Container> class readonly_container_facade { public: typedef typename Container::size_type size_type; typedef typename Container::const_iterator const_iterator; virtual ~readonly_container_facade() {} inline bool empty() const { return container.empty(); } inline const_iterator begin() const { return container.begin(); } inline const_iterator end() const { return container.end(); } inline size_type size() const { return container.size(); } protected: // hide to force inherited usage only readonly_container_facade() {} protected: // hide assignment by default readonly_container_facade(readonly_container_facade const& other): : container(other.container) {} readonly_container_facade& operator=(readonly_container_facade& other) { container = other.container; return *this; } protected: Container container; }; template <typename Container> class writable_container_facade: public readable_container_facade<Container> { public: typedef typename Container::iterator iterator; writable_container_facade(writable_container_facade& other) readonly_container_facade(other) {} virtual ~writable_container_facade() {} inline iterator begin() { return container.begin(); } inline iterator end() { return container.end(); } writable_container_facade& operator=(writable_container_facade& other) { readable_container_facade<Container>::operator=(other); return *this; } };
Diese Klassen stellen dieselbe Schnittstelle wie ein STL-Container bereit. Mir hat der Effekt gefallen, die modifizierenden und nicht modifizierenden Operationen in verschiedene Basisklassen zu unterteilen. Dies hat einen sehr schönen Effekt auf die Konstantenkorrektheit. Der einzige Nachteil ist, dass Sie die Schnittstelle erweitern müssen, wenn Sie diese mit assoziativen Containern verwenden möchten. Ich bin jedoch nicht auf die Not gestoßen.
quelle
In diesem Fall ist das Erben eine schlechte Idee: Die STL-Container verfügen nicht über virtuelle Destruktoren, sodass möglicherweise Speicherlecks auftreten (außerdem ist dies ein Hinweis darauf, dass STL-Container überhaupt nicht vererbt werden sollen).
Wenn Sie nur einige Funktionen hinzufügen müssen, können Sie diese in globalen Methoden oder in einer Lightweight-Klasse mit einem Containerelementzeiger / einer Referenz deklarieren. Natürlich können Sie Methoden nicht ausblenden: Wenn Sie wirklich danach suchen, gibt es keine andere Option, als die gesamte Implementierung neu zu deklarieren.
quelle
-Dprivate=public
in der Compiler-Befehlszeile verwenden kann. Zugriffsspezifizierer wie private sind meistens Dokumentationen, die zufällig erzwungen werden.Abgesehen von virtuellen Dtoren sollte die Entscheidung zum Erben im Vergleich zum Enthalten eine Entwurfsentscheidung sein, die auf der von Ihnen erstellten Klasse basiert. Sie sollten niemals Container-Funktionen erben, nur weil dies einfacher ist als das Enthalten eines Containers und das Hinzufügen einiger Funktionen zum Hinzufügen und Entfernen, die wie vereinfachte Wrapper erscheinen, es sei denn, Sie können definitiv sagen, dass die von Ihnen erstellte Klasse eine Art Container ist. Beispielsweise enthält eine Klassenklasse häufig Schülerobjekte, aber ein Klassenzimmer ist für die meisten Zwecke keine Art Liste von Schülern, sodass Sie nicht von der Liste erben sollten.
quelle
Es ist einfacher zu tun:
typedef std::vector<MyObject> MyContainer;
quelle
Die Weiterleitungsmethoden werden ohnehin weggeschnitten. Auf diese Weise erhalten Sie keine bessere Leistung. In der Tat werden Sie wahrscheinlich schlechtere Leistung erhalten.
quelle