Ist es in Ordnung, die Implementierung von STL-Containern zu erben, anstatt sie zu delegieren?

Lesezeit: 9 Minuten

Ist es in Ordnung die Implementierung von STL Containern zu erben
Emil Cormier

Ich habe eine Klasse, die std::vector anpasst, um einen Container mit domänenspezifischen Objekten zu modellieren. Ich möchte den größten Teil der std::vector-API dem Benutzer zur Verfügung stellen, damit er vertraute Methoden (Größe, Löschen, usw.) und Standardalgorithmen für den Container verwenden kann. Dies scheint ein wiederkehrendes Muster für mich in meinen Designs 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, bei der Wiederverwendung einer Klasse für die Implementierung die Komposition der Vererbung vorzuziehen – 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 wirklich so schlimm, die Implementierung in solchen Fällen zu erben? Was sind die Risiken? Gibt es eine sicherere Möglichkeit, dies ohne so viel Tipparbeit zu implementieren? Bin ich ein Ketzer für die Verwendung der Implementierungsvererbung? 🙂

Bearbeiten:

Wie wäre es, klarzustellen, 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 dieses Zeug die ganze Zeit zu machen.

Bearbeiten 2:

Einer der Vorschläge war, kostenlose Funktionen zu verwenden. Ich zeige es hier als Pseudo-Code:

typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...

Ein OO-Weg, es zu tun:

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

  • Oh gut! Eine weitere Chance, meinen Blog voranzutreiben punchlet.wordpress.com – Schreiben Sie im Grunde freie Funktionen und vergessen Sie den “mehr OO”-Wrapper-Ansatz. Es ist nicht mehr OO – wenn es so wäre, würde es Vererbung verwenden, was Sie in diesem Fall wahrscheinlich nicht sollten. Erinnere dich an OO != Klasse.

    anon

    9. Januar 2010 um 21:40 Uhr

  • @Neil: Aber, aber… globale Funktionen sind böse!!! Alles ist ein Objekt! 😉

    – Emil Cormier

    9. Januar 2010 um 21:48 Uhr


  • Sie sind nicht global, wenn Sie sie in einen Namensraum stellen.

    anon

    9. Januar 2010 um 21:49 Uhr

  • Wenn Sie wirklich die gesamte Schnittstelle von vector verfügbar machen möchten, ist es in C++ wahrscheinlich besser, die Komposition zu verwenden und einen Verweis auf den Vektor über einen Getter verfügbar zu machen (mit konstanten und nicht konstanten Versionen). In Java würdest du einfach erben, aber dann kommt in Java etwas numpty nicht mit, ignoriere deine Dokumentation, lösche dein Objekt durch den falschen Zeiger (oder erbe es erneut und vermassele es) und beschwere dich dann. Für ein begrenztes Publikum vielleicht, aber wenn Benutzer vielleicht Freaks für dynamische Polymorphie sind oder kürzlich Ex-Java-Programmierer sind, entwerfen Sie eine Schnittstelle, bei der Sie ziemlich sicher sein können, dass sie missverstanden werden.

    – Steve Jessop

    9. Januar 2010 um 23:06 Uhr


  • Sie können sich nicht davor schützen, dass die Dokumentation vollständig ignoriert wird. Ich wäre nicht überrascht, wenn ich herausfinden würde, dass ein solcher Missbrauch in Java genauso viele Probleme verursacht wie in C++.

    Roger Pate

    11. April 2010 um 2:08 Uhr

Das Risiko wird durch einen Zeiger auf die Basisklasse aufgehoben (löschen, löschen[], und möglicherweise andere Freigabemethoden). Da diese Klassen (deque, Karte, Schnuretc.) keine virtuellen Dtors haben, ist es unmöglich, sie mit nur einem Zeiger auf diese Klassen richtig zu bereinigen:

struct BadExample : vector<int> {};
int main() {
  vector<int>* p = new BadExample();
  delete p; // this is Undefined Behavior
  return 0;
}

Das gesagt, wenn Sie bereit sind sicherzustellen, dass Sie dies niemals versehentlich tun, gibt es kaum einen großen Nachteil, sie zu erben – aber in manchen 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 (Schnur bestimmtes). In einigen Fällen ist jedoch eine Vererbung vorgesehen, wie z. B. Containeradapter Stapel ein geschütztes Mitglied haben C (der zugrunde liegende Container, den sie anpassen), und es ist fast nur von einer abgeleiteten Klasseninstanz aus darauf zugegriffen werden.

Anstatt entweder Erbschaft oder Zusammensetzung, Denken Sie darüber nach, freie Funktionen zu schreiben die entweder ein Iteratorpaar oder eine Containerreferenz nehmen und damit arbeiten. Praktisch ganz ist ein Beispiel dafür; und make_haufen, pop_haufenund push_haufeninsbesondere , sind ein Beispiel für die Verwendung freier Funktionen anstelle eines domänenspezifischen Containers.

Verwenden Sie also die Containerklassen für Ihre Datentypen und rufen Sie trotzdem die freien Funktionen für Ihre domänenspezifische Logik auf. Aber Sie können immer noch eine gewisse Modularität erreichen, indem Sie eine Typedef verwenden, die es Ihnen ermöglicht, sie sowohl zu vereinfachen als auch einen einzigen Punkt bereitzustellen, 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 value_type und allocator sich ändern können, ohne späteren Code mit typedef zu beeinflussen, und sogar der Container kann sich von a ändern deque zu einem Vektor.

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 „im Sinne von 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;
}

  • Ich kann nicht umhin, das zu erwähnen private Vererbung ist immer noch Vererbung und damit eine stärkere Beziehung als Komposition. Insbesondere bedeutet dies, dass eine Änderung der Implementierung Ihrer Klasse zwangsläufig die Binärkompatibilität beeinträchtigen wird.

    – Matthias M.

    10. Januar 2010 um 14:13 Uhr

  • Sowohl private Vererbung als auch private Datenmitglieder unterbrechen die Binärkompatibilität, wenn sie sich ändern, und abgesehen von Freunden (die wenige sein sollten) ist es normalerweise nicht schwer, zwischen ihnen zu wechseln – was verwendet wird, hängt oft von Implementierungsdetails ab. Siehe auch das “Base-from-Member-Idiom”.

    Roger Pate

    10. Januar 2010 um 14:22 Uhr

  • Für die Neugierigen – Basis-von-Mitglieder-Idiom: en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Base-from-Member

    – Emil Cormier

    16. März 2014 um 13:46 Uhr


  • @MatthieuM. Das Brechen von ABI ist für die meisten Anwendungen überhaupt kein Problem. Sogar einige Bibliotheken leben ohne Pimpl für eine bessere Leistung.

    – mip

    24. November 2014 um 10:00 Uhr


Wie alle bereits gesagt haben, haben STL-Container keine virtuellen Destruktoren, so dass das Erben von ihnen bestenfalls unsicher ist. Ich habe die generische Programmierung mit Templates immer als einen anderen OO-Stil betrachtet – einen ohne Vererbung. Die Algorithmen definieren die Schnittstelle, die sie benötigen. Es ist so nah Duck-Typisierung wie Sie es in einer statischen Sprache bekommen können.

Jedenfalls habe ich etwas zur Diskussion 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.

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 gefiel der Effekt der Trennung der modifizierenden und nicht modifizierenden Operationen in unterschiedliche Basisklassen. Dies hat einen wirklich schönen Effekt auf die const-Korrektheit. 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 Notwendigkeit gestoßen.

  • Hübsch! Das könnte ich einfach verwenden. Aber andere haben über die Idee nachgedacht, Container anzupassen, also werde ich sie vielleicht nicht verwenden. 🙂

    – Emil Cormier

    9. Januar 2010 um 23:08 Uhr

  • Allerdings kann eine aufwändige Template-Programmierung zu ebenso schlechtem Spaghetti-Code, riesigen Bibliotheken, schlechter Isolierung der Funktionalität und unverständlichen Kompilierzeitfehlern führen.

    – Erik Aronesty

    6. Februar 2020 um 22:32 Uhr

In diesem Fall ist Vererbung eine schlechte Idee: Die STL-Container haben keine virtuellen Destruktoren, sodass Sie möglicherweise auf Speicherlecks stoßen (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 sie in globalen Methoden oder einer einfachen Klasse mit einem Container-Member-Zeiger/einer Referenz deklarieren. Dies erlaubt Ihnen natürlich nicht, Methoden zu verstecken: Wenn Sie wirklich danach suchen, gibt es keine andere Möglichkeit, als die gesamte Implementierung neu zu deklarieren.

Abgesehen von virtuellen Dtors sollte die Entscheidung, zu erben oder zu enthalten, eine Designentscheidung sein, die auf der Klasse basiert, die Sie erstellen. Sie sollten die Containerfunktionalität niemals erben, nur weil es einfacher ist, als einen Container zu enthalten und ein paar Hinzufügungs- und Entfernungsfunktionen hinzuzufügen, die wie simple Wrapper erscheinen wenn nicht Sie können definitiv sagen, dass die Klasse, die Sie erstellen, eine Art Container ist. Beispielsweise enthält eine Klassenzimmerklasse häufig Schülerobjekte, aber ein Klassenzimmer ist für die meisten Zwecke keine Art Liste von Schülern, daher sollten Sie nicht von der Liste erben.

Ist es in Ordnung die Implementierung von STL Containern zu erben
Martin York

Es ist einfacher zu tun:

typedef std::vector<MyObject> MyContainer;

1647074412 434 Ist es in Ordnung die Implementierung von STL Containern zu erben
Charles Eli Käse

Die Weiterleitungsmethoden werden sowieso inliniert. Auf diese Weise werden Sie keine bessere Leistung erzielen. Tatsächlich werden Sie wahrscheinlich eine schlechtere Leistung erzielen.

993170cookie-checkIst es in Ordnung, die Implementierung von STL-Containern zu erben, anstatt sie zu delegieren?

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

Privacy policy