Warum sollte man verschachtelte Klassen in C ++ verwenden?

188

Kann mich bitte jemand auf einige nette Ressourcen hinweisen, um verschachtelte Klassen zu verstehen und zu verwenden? Ich habe Material wie Programmierprinzipien und solche Dinge wie dieses IBM Knowledge Center - Verschachtelte Klassen

Aber ich habe immer noch Probleme, ihren Zweck zu verstehen. Könnte mir bitte jemand helfen?

mit Brille
quelle
15
Mein Rat für verschachtelte Klassen in C ++ ist einfach, keine verschachtelten Klassen zu verwenden.
Billy ONeal
7
Sie sind genau wie normale Klassen ... außer verschachtelt. Verwenden Sie sie, wenn die interne Implementierung einer Klasse so komplex ist, dass sie am einfachsten von mehreren kleineren Klassen modelliert werden kann.
Meagar
12
@ Billy: Warum? Scheint mir zu breit.
John Dibling
30
Ich habe immer noch kein Argument gesehen, warum verschachtelte Klassen von Natur aus schlecht sind.
John Dibling
7
@ 7vies: 1. weil es einfach nicht notwendig ist - Sie können dasselbe mit extern definierten Klassen tun, was den Umfang einer bestimmten Variablen verringert, was eine gute Sache ist. 2. weil Sie alles tun können, was verschachtelte Klassen tun können typedef. 3. weil sie in einer Umgebung, in der es bereits schwierig ist, lange Zeilen zu vermeiden, eine zusätzliche Einrückungsstufe hinzufügen 4. weil Sie zwei konzeptionell getrennte Objekte in einer einzigen classDeklaration deklarieren usw.
Billy ONeal

Antworten:

228

Verschachtelte Klassen sind cool, um Implementierungsdetails auszublenden.

Aufführen:

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

Hier möchte ich Node nicht verfügbar machen, da andere Benutzer sich möglicherweise für die Verwendung der Klasse entscheiden. Dies würde mich daran hindern, meine Klasse zu aktualisieren, da alles, was verfügbar gemacht wird, Teil der öffentlichen API ist und für immer beibehalten werden muss . Indem ich die Klasse privat mache, verstecke ich nicht nur die Implementierung, sondern sage auch, dass dies meine ist, und ich kann sie jederzeit ändern, sodass Sie sie nicht verwenden können.

Schauen Sie sich an std::listoder std::mapsie enthalten alle versteckte Klassen (oder doch?). Der Punkt ist, dass sie möglicherweise oder möglicherweise nicht, aber da die Implementierung privat und versteckt ist, konnten die Builder der STL den Code aktualisieren, ohne die Verwendung des Codes zu beeinflussen, oder viel altes Gepäck in der STL herumliegen lassen, weil sie es benötigen um die Abwärtskompatibilität mit einem Dummkopf aufrechtzuerhalten, der beschlossen hat, die darin verborgene Node-Klasse zu verwenden list.

Martin York
quelle
9
Wenn Sie dies tun, Nodesollte es überhaupt nicht in der Header-Datei verfügbar gemacht werden.
Billy ONeal
6
@ Billy ONeal: Was ist, wenn ich eine Header-Datei-Implementierung wie STL oder Boost mache?
Martin York
5
@ Billy ONeal: Nein. Es ist eine Frage des guten Designs, nicht der Meinung. Das Einfügen in einen Namespace schützt es nicht vor der Verwendung. Es ist jetzt Teil der öffentlichen API, die für die Ewigkeit gepflegt werden muss.
Martin York
21
@ Billy ONeal: Es schützt es vor versehentlichem Gebrauch. Es dokumentiert auch die Tatsache, dass es privat ist und nicht verwendet werden sollte (kann nicht verwendet werden, es sei denn, Sie tun etwas Dummes). Sie müssen es also nicht unterstützen. Wenn Sie es in einen Namespace einfügen, wird es Teil der öffentlichen API (etwas, das Sie in dieser Konversation immer wieder vermissen. Öffentliche API bedeutet, dass Sie es unterstützen müssen).
Martin York
10
@Billy ONeal: Verschachtelte Klasse hat einen Vorteil gegenüber verschachteltem Namespace: Sie können keine Instanzen eines Namespace erstellen, aber Sie können Instanzen einer Klasse erstellen. Zur detailKonvention: Abhängig von solchen Konventionen muss man sich selbst im Auge behalten. Es ist besser, sich auf den Compiler zu verlassen, der sie für Sie verfolgt.
SasQ
142

Verschachtelte Klassen sind wie normale Klassen, aber:

  • Sie haben zusätzliche Zugriffsbeschränkungen (wie alle Definitionen innerhalb einer Klassendefinition).
  • Sie verschmutzen den angegebenen Namespace nicht , z. B. den globalen Namespace. Wenn Sie der Meinung sind, dass Klasse B so eng mit Klasse A verbunden ist, die Objekte von A und B jedoch nicht unbedingt miteinander verbunden sind, möchten Sie möglicherweise, dass auf die Klasse B nur über das Scoping der A-Klasse zugegriffen werden kann (dies wird als A bezeichnet) ::Klasse).

Einige Beispiele:

Klasse öffentlich verschachteln, um sie in einen Bereich der relevanten Klasse zu setzen


Angenommen, Sie möchten eine Klasse haben, die Klassenobjekte SomeSpecificCollectionaggregiert Element. Sie können dann entweder:

  1. Deklarieren Sie zwei Klassen: SomeSpecificCollectionund Element- schlecht, da der Name "Element" allgemein genug ist, um einen möglichen Namenskonflikt zu verursachen

  2. Führen Sie einen Namespace ein someSpecificCollectionund deklarieren Sie Klassen someSpecificCollection::Collectionund someSpecificCollection::Element. Kein Risiko eines Namenskonflikts, aber kann es noch ausführlicher werden?

  3. deklarieren Sie zwei globale Klassen SomeSpecificCollectionund SomeSpecificCollectionElement- was kleinere Nachteile hat, aber wahrscheinlich in Ordnung ist.

  4. Deklarieren Sie globale Klasse SomeSpecificCollectionund Klasse Elementals verschachtelte Klasse. Dann:

    • Sie riskieren keine Namenskonflikte, da sich Element nicht im globalen Namespace befindet.
    • In der Implementierung von SomeSpecificCollectionbeziehen Sie sich auf gerecht Elementund überall sonst als SomeSpecificCollection::Element- was + aussieht - das gleiche wie 3., aber klarer
    • Es wird ganz einfach, dass es "ein Element einer bestimmten Sammlung" ist, nicht "ein bestimmtes Element einer Sammlung".
    • Es ist sichtbar, dass dies SomeSpecificCollectionauch eine Klasse ist.

Meiner Meinung nach ist die letzte Variante definitiv das intuitivste und damit beste Design.

Lassen Sie mich betonen - Es ist kein großer Unterschied, zwei globale Klassen mit ausführlicheren Namen zu erstellen. Es ist nur ein kleines Detail, aber imho macht es den Code klarer.

Einführung eines anderen Bereichs innerhalb eines Klassenbereichs


Dies ist besonders nützlich für die Einführung von Typedefs oder Enums. Ich werde hier nur ein Codebeispiel posten:

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

Man wird dann anrufen:

Product p(Product::FANCY, Product::BOX);

Wenn man sich jedoch Vorschläge zur Code-Vervollständigung ansieht Product::, werden oft alle möglichen Aufzählungswerte (BOX, FANCY, CRATE) aufgelistet, und es ist leicht, hier einen Fehler zu machen (die stark typisierten Aufzählungen von C ++ 0x lösen das irgendwie, aber egal ).

Wenn Sie jedoch mithilfe verschachtelter Klassen zusätzlichen Bereich für diese Aufzählungen einführen, könnte dies folgendermaßen aussehen:

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

Dann sieht der Anruf so aus:

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

Product::ProductType::Wenn Sie dann eine IDE eingeben , erhalten Sie nur die Aufzählungen aus dem gewünschten vorgeschlagenen Bereich. Dies verringert auch das Fehlerrisiko.

Natürlich wird dies für kleine Klassen möglicherweise nicht benötigt, aber wenn man viele Aufzählungen hat, erleichtert dies den Client-Programmierern die Arbeit.

Auf die gleiche Weise könnten Sie eine große Anzahl von Typedefs in einer Vorlage "organisieren", falls Sie dies jemals benötigen würden. Es ist manchmal ein nützliches Muster.

Die PIMPL-Sprache


Die PIMPL (kurz für Pointer to IMPLementation) ist eine nützliche Redewendung, um die Implementierungsdetails einer Klasse aus dem Header zu entfernen. Dies reduziert die Notwendigkeit, Klassen abhängig vom Header der Klasse neu zu kompilieren, wenn sich der "Implementierung" -Teil des Headers ändert.

Es wird normalerweise mit einer verschachtelten Klasse implementiert:

Xh:

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

X.cpp:

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

Dies ist besonders nützlich, wenn für die vollständige Klassendefinition die Definition von Typen aus einer externen Bibliothek erforderlich ist, die eine schwere oder nur hässliche Header-Datei enthält (nehmen Sie WinAPI). Wenn Sie PIMPL verwenden, können Sie WinAPI-spezifische Funktionen nur in einschließen .cppund niemals einschließen .h.

Kos
quelle
3
struct Impl; std::auto_ptr<Impl> impl; Dieser Fehler wurde von Herb Sutter populär gemacht. Verwenden Sie auto_ptr nicht für unvollständige Typen oder treffen Sie zumindest Vorsichtsmaßnahmen, um zu vermeiden, dass falscher Code generiert wird.
Gene Bushuyev
2
@Billy ONeal: Soweit mir bekannt ist, können Sie auto_ptrin den meisten Implementierungen einen unvollständigen Typ deklarieren , aber technisch gesehen ist es UB im Gegensatz zu einigen Vorlagen in C ++ 0x (z. B. unique_ptr), bei denen explizit angegeben wurde, dass der Vorlagenparameter möglicherweise ist ein unvollständiger Typ und wo genau der Typ vollständig sein muss. (zB Verwendung von ~unique_ptr)
CB Bailey
2
@Billy ONeal: In C ++ 03 17.4.6.3 [lib.res.on.functions] heißt es: "Insbesondere sind die Auswirkungen in den folgenden Fällen undefiniert: [...] wenn ein unvollständiger Typ als Vorlagenargument verwendet wird beim Instanziieren einer Vorlagenkomponente. " In C ++ 0x heißt es: "Wenn beim Instanziieren einer Vorlagenkomponente ein unvollständiger Typ als Vorlagenargument verwendet wird, sofern dies nicht speziell für diese Komponente zulässig ist." und später (zB): "Der Vorlagenparameter Tvon unique_ptrkann ein unvollständiger Typ sein."
CB Bailey
1
@MilesRout Das ist viel zu allgemein. Hängt davon ab, ob Clientcode erben darf. Regel: Wenn Sie sicher sind, dass Sie nicht über einen Basisklassenzeiger löschen, ist der virtuelle dtor vollständig redundant.
Kos
2
@IsaacPascual aww, ich sollte das jetzt aktualisieren, da wir haben enum class.
Kos
21

Ich benutze nicht viel verschachtelte Klassen, aber ich benutze sie ab und zu. Insbesondere, wenn ich einen Datentyp definiere und dann einen STL-Funktor definieren möchte, der für diesen Datentyp entwickelt wurde.

Betrachten Sie beispielsweise eine generische FieldKlasse mit einer ID-Nummer, einem Typcode und einem Feldnamen. Wenn ich eines vectordieser Fields nach ID-Nummer oder Name durchsuchen möchte , kann ich einen Funktor dafür erstellen:

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

Dann kann Code, der nach diesen Fields suchen muss, den matchGültigkeitsbereich innerhalb der FieldKlasse selbst verwenden:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
John Dibling
quelle
Vielen Dank für das großartige Beispiel und die Kommentare, obwohl ich mit STL-Funktionen nicht ganz vertraut bin. Ich stelle fest, dass die Konstruktoren in match () öffentlich sind. Ich gehe davon aus, dass Konstruktoren nicht immer öffentlich sein müssen. In diesem Fall können sie nicht außerhalb der Klasse instanziiert werden.
Brille
1
@user: Bei einem STL-Funktor muss der Konstruktor öffentlich sein.
John Dibling
1
@Billy: Ich noch habe noch keine konkreten Überlegungen , um zu sehen , warum verschachtelte Klassen schlecht sind.
John Dibling
@ John: Alle Richtlinien für den Codierungsstil sind Ansichtssache. Ich habe hier in mehreren Kommentaren mehrere Gründe aufgeführt, die (meiner Meinung nach) alle vernünftig sind. Es gibt kein "sachliches" Argument, das vorgebracht werden kann, solange der Code gültig ist und kein undefiniertes Verhalten hervorruft. Ich denke jedoch, dass das Codebeispiel, das Sie hier einfügen, einen großen Grund aufzeigt, warum ich verschachtelte Klassen vermeide - nämlich die Namenskonflikte.
Billy ONeal
1
Natürlich gibt es technische Gründe, Inlines Makros vorzuziehen !!
Miles Rout
14

Man kann ein Builder-Muster mit verschachtelter Klasse implementieren . Besonders in C ++ finde ich es semantisch sauberer. Beispielsweise:

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

Eher, als:

class Product {}
class ProductBuilder {}
Yeo
quelle
Sicher, es wird funktionieren, wenn es nur einen Build gibt, aber es wird böse, wenn mehrere Betonbauer benötigt werden. Man sollte sorgfältig Designentscheidungen treffen :)
Irsis