Perché dovremmo usare le classi annidate in C ++?

Qualcuno può indicarmi delle buone risorse per capire e usare le classi annidate? Ho materiale come i Principi di programmazione e cose come questo IBM Knowledge Center – Classi annidate

Ma ho ancora difficoltà a capire il loro scopo. Qualcuno può aiutarmi?

Le classi annidate sono interessanti per hide i dettagli di implementazione

Elenco:

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

Qui non voglio esporre Node in quanto altre persone potrebbero decidere di utilizzare la class e questo mi impedirebbe di aggiornare la mia class in quanto qualsiasi cosa esposta fa parte dell’API pubblica e deve essere mantenuta per sempre . Rendendo la class privata, non solo nascondo l’implementazione, sto anche dicendo che questo è mio e potrei cambiarlo in qualsiasi momento in modo da non poterlo usare.

Guarda std::list o std::map contengono tutti classi nascoste (o fanno?). Il punto è che possono o no, ma poiché l’implementazione è privata e nascosta, i costruttori della STL sono stati in grado di aggiornare il codice senza influenzare il modo in cui si è utilizzato il codice o di lasciare un sacco di vecchi bagagli attorno all’STL perché hanno bisogno di per mantenere la retrocompatibilità con un pazzo che ha deciso di voler usare la class Node che era nascosta nella list .

Le classi annidate sono come normali classi, ma:

  • hanno una restrizione di accesso aggiuntiva (come fanno tutte le definizioni all’interno di una definizione di class),
  • non inquinano lo spazio dei nomi dato , ad es. spazio dei nomi globale. Se ritieni che la class B sia così profondamente connessa alla class A, ma gli oggetti di A e B non sono necessariamente correlati, allora potresti volere che la class B sia accessibile solo tramite lo scope della class A (sarebbe chiamata A ::Classe).

Qualche esempio:

Classificare pubblicamente la class per inserirla in un ambito della class pertinente


Supponiamo di voler avere una class SomeSpecificCollection che aggrega oggetti di class Element . È quindi ansible:

  1. dichiarare due classi: SomeSpecificCollection ed Element – bad, perché il nome “Element” è abbastanza generale da causare un ansible conflitto di nomi

  2. introdurre uno spazio someSpecificCollection nomi someSpecificCollection e dichiarare classi someSpecificCollection::Collection e someSpecificCollection::Element . Nessun rischio di scontro sul nome, ma può diventare più dettagliato?

  3. dichiarare due classi globali SomeSpecificCollection e SomeSpecificCollectionElement – che presenta svantaggi minori, ma probabilmente è OK.

  4. dichiara class globale SomeSpecificCollection e class Element come class nidificata. Poi:

    • non rischi alcun conflitto di nomi poiché Element non si trova nello spazio dei nomi globale,
    • nell’implementazione di SomeSpecificCollection fai riferimento solo a Element e ovunque altro come SomeSpecificCollection::Element – che sembra + – uguale a 3., ma più chiaro
    • diventa chiaro che è “un elemento di una collezione specifica”, non “un elemento specifico di una collezione”
    • è visibile che SomeSpecificCollection è anche una class.

A mio parere, l’ultima variante è sicuramente il design più intuitivo e quindi migliore.

Lasciatemi sottolineare – Non è una grande differenza dal fare due classi globali con nomi più dettagliati. È solo un piccolo piccolo dettaglio, ma secondo me rende il codice più chiaro.

Presentazione di un altro ambito nell’ambito di una class


Questo è particolarmente utile per introdurre typedef o enumerazioni. Inserirò semplicemente un esempio di codice qui:

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

Uno quindi chiamerà:

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

Ma quando si esaminano le proposte di completamento del codice per Product:: , si otterranno spesso tutti i possibili valori enum (BOX, FANCY, CRATE) ed è facile fare un errore qui (il tipo enumerato di C ++ 0x risolve il problema, ma non importa).

Ma se introduci uno scope aggiuntivo per queste enumerazioni usando classi nidificate, le cose potrebbero assomigliare:

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

Quindi la chiamata ha il seguente aspetto:

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

Quindi digitando Product::ProductType:: in un IDE, si otterrà solo l’enumerazione dall’ambito desiderato suggerito. Ciò riduce anche il rischio di commettere un errore.

Naturalmente questo potrebbe non essere necessario per le classi piccole, ma se uno ha un sacco di enumerazioni, allora rende le cose più facili per i programmatori client.

Allo stesso modo, potresti “organizzare” un grande gruppo di typedef in un modello, se mai ne hai avuto la necessità. A volte è un modello utile.

L’idioma PIMPL


PIMPL (abbreviazione di Pointer to IMPLementation) è un idioma utile per rimuovere i dettagli di implementazione di una class dall’intestazione. Questo riduce la necessità di ricompilare le classi a seconda dell’intestazione della class ogni volta che cambia la parte “implementazione” dell’intestazione.

Di solito è implementato usando una class nidificata:

Xh:

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

X.cpp:

 #include "Xh" #include  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 

Ciò è particolarmente utile se la definizione completa della class richiede la definizione di tipi da una libreria esterna che ha un file di intestazione pesante o appena brutto (prendi WinAPI). Se si utilizza PIMPL, è ansible includere qualsiasi funzionalità specifica di WinAPI solo in .cpp e non includerla mai in .h .

Non uso molto le classi annidate, ma le uso di tanto in tanto. Soprattutto quando definisco un tipo di tipo di dati, e poi voglio definire un funtore STL progettato per quel tipo di dati.

Ad esempio, si consideri una class Field generica che ha un numero ID, un codice tipo e un nome campo. Se voglio cercare un vector di questi Field per numero ID o nome, potrei build un funtore per farlo:

 class Field { public: unsigned id_; string name_; unsigned type_; class match : public std::unary_function { 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_; }; }; 

Quindi il codice che ha bisogno di cercare questi Field può utilizzare la ricerca nell’ambito della class Field stessa:

 vector::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName")); 

Si può implementare un modello di Builder con una class annidata . Soprattutto in C ++, personalmente lo trovo semanticamente più pulito. Per esempio:

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

Piuttosto che:

 class Product {} class ProductBuilder {}