Perché usare le classi nidificate in C ++?


188

Qualcuno può indicarmi alcune risorse utili per comprendere e usare le classi nidificate? Ho del materiale come Principi di programmazione e cose come questo IBM Knowledge Center - Nested Classes

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


15
Il mio consiglio per le classi nidificate in C ++ è semplicemente di non usare le classi nidificate.
Billy ONeal,

7
Sono esattamente come le lezioni normali ... tranne che nidificate. Usali quando l'implementazione interna di una classe è così complessa che può essere facilmente modellata da più classi più piccole.
meagar

12
@Billy: Perché? Mi sembra troppo ampio.
John Dibling,

30
Non ho ancora visto un argomento sul perché le classi nidificate sono cattive per loro natura.
John Dibling,

7
@ 7vies: 1. perché semplicemente non è necessario: puoi fare lo stesso con classi definite esternamente, il che riduce l'ambito di una data variabile, il che è positivo. 2. perché puoi fare tutto ciò che le classi nidificate possono fare typedef. 3. perché aggiungono un ulteriore livello di rientro in un ambiente in cui evitare lunghe righe è già difficile 4. perché si dichiarano due oggetti concettualmente separati in una singola classdichiarazione, ecc.
Billy ONeal

Risposte:


229

Le classi nidificate sono interessanti per nascondere i dettagli dell'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 poiché altre persone potrebbero decidere di utilizzare la classe e ciò mi impedirebbe di aggiornare la mia classe poiché qualsiasi cosa esposta fa parte dell'API pubblica e deve essere mantenuta per sempre . Rendendo la classe privata, non solo nascondo l'implementazione, ma sto anche dicendo che questa è mia e posso cambiarla in qualsiasi momento in modo da non poterla usare.

Guarda std::listo std::mapcontengono tutti classi nascoste (o lo fanno?). Il punto è che possono o meno, ma poiché l'implementazione è privata e nascosta, i costruttori di STL sono stati in grado di aggiornare il codice senza influenzare il modo in cui lo hai usato o lasciare un sacco di vecchi bagagli che giacciono attorno allo STL perché hanno bisogno per mantenere la retrocompatibilità con alcuni sciocchi che decisero di voler usare la classe Node che era nascosta all'interno list.


9
Se lo stai facendo Node, non dovrebbe essere esposto affatto nel file di intestazione.
Billy ONeal,

6
@Billy ONeal: cosa succede se sto eseguendo un'implementazione del file di intestazione come STL o boost.
Martin York,

6
@Billy ONeal: No. È una questione di buon design, non di opinione. Metterlo in uno spazio dei nomi non lo protegge dall'uso. Ora fa parte dell'API pubblica che deve essere mantenuta per sempre.
Martin York,

21
@Billy ONeal: lo protegge dall'uso accidentale. Documenta anche il fatto che è privato e non dovrebbe essere usato (non può essere usato se non fai qualcosa di stupido). Pertanto non è necessario supportarlo. Metterlo in uno spazio dei nomi lo rende parte dell'API pubblica (qualcosa che manchi in questa conversazione. L'API pubblica significa che devi supportarlo).
Martin York,

10
@Billy ONeal: la classe nidificata presenta alcuni vantaggi rispetto allo spazio dei nomi nidificati: non è possibile creare istanze di uno spazio dei nomi, ma è possibile creare istanze di una classe. Per quanto riguarda la detailconvenzione: invece, a seconda di tali convenzioni, è necessario tenere a mente se stessi, è meglio dipendere dal compilatore che ne tiene traccia.
SasQ,

142

Le classi nidificate sono proprio come le classi normali, ma:

  • hanno restrizioni di accesso aggiuntive (come fanno tutte le definizioni all'interno di una definizione di classe),
  • che non inquinano il dato spazio dei nomi , ad esempio namespace globale. Se ritieni che la classe B sia così profondamente connessa alla classe A, ma gli oggetti di A e B non siano necessariamente correlati, allora potresti desiderare che la classe B sia accessibile solo tramite l'ambito della classe A (sarebbe indicata come A ::Classe).

Qualche esempio:

Classe di nidificazione pubblica per inserirlo in un ambito della classe pertinente


Supponiamo di voler avere una classe SomeSpecificCollectionche aggregi oggetti di classe Element. È quindi possibile:

  1. dichiarare due classi: SomeSpecificCollectione Element- cattivo, perché il nome "Elemento" è abbastanza generale da causare un possibile scontro tra nomi

  2. introdurre uno spazio dei nomi someSpecificCollectione dichiarare le classi someSpecificCollection::Collectione someSpecificCollection::Element. Nessun rischio di scontro tra nomi, ma può essere più dettagliato?

  3. dichiarare due classi globali SomeSpecificCollectione SomeSpecificCollectionElement- che ha degli svantaggi minori, ma probabilmente va bene.

  4. dichiarare la classe globale SomeSpecificCollectione la classe Elementcome classe nidificata. Poi:

    • non rischi alcun conflitto di nomi poiché Element non si trova nello spazio dei nomi globale,
    • in attuazione di SomeSpecificCollectionte fai riferimento a just Element, e ovunque come SomeSpecificCollection::Element- che sembra + - uguale a 3., ma più chiaro
    • diventa semplice che è "un elemento di una raccolta specifica", non "un elemento specifico di una raccolta"
    • è visibile che SomeSpecificCollectionè anche una classe.

Secondo me, l'ultima variante è sicuramente il design più intuitivo e quindi il migliore.

Consentitemi di sottolineare: non è una grande differenza rispetto alla creazione di due classi globali con nomi più dettagliati. È solo un piccolo dettaglio, ma lo rende più chiaro.

Presentazione di un altro ambito all'interno di un ambito di classe


Ciò è particolarmente utile per introdurre typedef o enumerazioni. Pubblicherò solo 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 Product::, si ottengono spesso tutti i possibili valori enum (BOX, FANCY, CRATE) elencati ed è facile fare un errore qui (gli enum fortemente tipizzati di C ++ 0x risolvono questo problema, ma non importa ).

Ma se si introduce un ambito aggiuntivo per quegli enum usando le classi nidificate, le cose potrebbero apparire come:

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 appare come:

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

Quindi digitando Product::ProductType::un IDE, si otterranno solo gli enumerazione dall'ambito desiderato suggerito. Ciò riduce anche il rischio di fare un errore.

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

Allo stesso modo, potresti "organizzare" un grande gruppo di dattiloscritti in un modello, se mai ne avessi bisogno. È un modello utile a volte.

Il linguaggio di PIMPL


PIMPL (abbreviazione di Pointer to IMPLementation) è un linguaggio utile per rimuovere dall'intestazione i dettagli di implementazione di una classe. Ciò riduce la necessità di ricompilare le classi a seconda dell'intestazione della classe ogni volta che la parte "implementazione" dell'intestazione cambia.

Di solito è implementato usando una classe nidificata:

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

Ciò è particolarmente utile se la definizione di classe completa necessita della definizione di tipi da una libreria esterna che ha un file di intestazione pesante o semplicemente brutto (prendi WinAPI). Se si utilizza PIMPL, è possibile racchiudere qualsiasi funzionalità specifica di WinAPI solo in .cppe non includerla mai .h.


3
struct Impl; std::auto_ptr<Impl> impl; Questo errore è stato reso popolare da Herb Sutter. Non utilizzare auto_ptr su tipi incompleti o almeno prendere precauzioni per evitare la generazione di codice errato.
Gene Bushuyev il

2
@Billy ONeal: per quanto ne so, puoi dichiarare un auto_ptrtipo incompleto nella maggior parte delle implementazioni, ma tecnicamente è UB a differenza di alcuni dei modelli in C ++ 0x (ad esempio unique_ptr) in cui è stato reso esplicito che il parametro template può essere un tipo incompleto e dove esattamente il tipo deve essere completo. (es. uso di ~unique_ptr)
CB Bailey

2
@Billy ONeal: In C ++ 03 17.4.6.3 [lib.res.on.functions] dice "In particolare, gli effetti non sono definiti nei seguenti casi: [...] se un tipo incompleto viene usato come argomento template quando si crea un'istanza di un componente modello ". mentre in C ++ 0x si dice "se un tipo incompleto viene usato come argomento template quando si crea un'istanza di un componente template, a meno che non sia specificamente consentito per quel componente." e versioni successive (ad es.): "Il parametro template Tdi unique_ptrpuò essere un tipo incompleto."
CB Bailey

1
@MilesRout È troppo generico. Dipende se il codice client può ereditare. Regola: se si è certi di non eliminare tramite un puntatore della classe base, il dtor virtuale è completamente ridondante.
Kos,

2
@IsaacPascual aww, dovrei aggiornarlo ora che abbiamo enum class.
Kos,

21

Non uso molto le classi nidificate, ma le utilizzo di tanto in tanto. Soprattutto quando definisco un tipo di tipo di dati e quindi desidero definire un funzione STL progettato per quel tipo di dati.

Ad esempio, considera una Fieldclasse generica che ha un numero ID, un codice di tipo e un nome campo. Se voglio cercare uno vectordi questi messaggi Fieldcon un numero ID o nome, potrei costruire un funzione per farlo:

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

Quindi il codice che deve cercare questi messaggi Fieldpuò usare l' matchambito all'interno della Fieldclasse stessa:

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

Grazie per il grande esempio e commenti, anche se non sono del tutto consapevole delle funzioni STL. Ho notato che i costruttori in match () sono pubblici. Presumo che i costruttori non debbano sempre essere pubblici, nel qual caso non possono essere istanziati al di fuori della classe.
occhialuto il

1
@utente: nel caso di un funzione STL, il costruttore deve essere pubblico.
John Dibling,

1
@Billy: devo ancora vedere qualche ragionamento concreto per cui le classi nidificate sono cattive.
John Dibling,

@John: tutte le linee guida sullo stile di codifica si basano su una questione di opinione. Ho elencato diverse ragioni in diversi commenti qui, tutti (a mio avviso) sono ragionevoli. Non ci sono argomenti "fattuali" che possono essere fatti fintanto che il codice è valido e non invoca comportamenti indefiniti. Tuttavia, penso che l'esempio di codice che metti qui metta in evidenza un grande motivo per cui evito le classi nidificate, vale a dire gli scontri con i nomi.
Billy ONeal,

1
Naturalmente ci sono ragioni tecniche per preferire le linee in linea alle macro !!
Miles Rout

14

È possibile implementare un modello Builder con classe nidificata . 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 {}

Certo, funzionerà se esiste una sola build, ma diventerà sgradevole se è necessario disporre di più costruttori di cemento. Bisogna prendere con cura le decisioni di progettazione :)
irsis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.