Domanda

Sto cercando la definizione di quando mi è consentito eseguire la dichiarazione anticipata di una classe nel file di intestazione di un'altra classe:

Posso farlo per una classe base, per una classe tenuta come membro, per una classe passata alla funzione membro per riferimento, ecc.?

È stato utile?

Soluzione

Mettetevi nella posizione del compilatore: quando si avanti dichiarare un tipo, tutto il compilatore sa è che questo tipo esiste; si sa nulla sulle sue dimensioni, membri, o metodi. Questo è il motivo per cui si chiama un tipo incompleto . Pertanto, non è possibile utilizzare il tipo di dichiarare un membro, o di una classe base, dal momento che il compilatore avrebbe bisogno di conoscere il layout del tipo.

Supponendo che la seguente dichiarazione in avanti.

class X;

Ecco cosa si può e non può fare.

Che cosa si può fare con un tipo incompleto:

  • dichiarare un membro di un puntatore o un riferimento al tipo incompleto:

    class Foo {
        X *p;
        X &r;
    };
    
  • Dichiarare funzioni o metodi che accettino / Tipi restituiti incompleti:

    void f1(X);
    X    f2();
    
  • Definisci funzioni o metodi che accettano puntatori / ritorno / riferimenti al tipo incompleto (ma senza usare i suoi membri):

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}
    

Che cosa non si può fare con un tipo incompleto:

  • usarlo come classe base

    class Foo : X {} // compiler error!
    
  • Si usa per dichiarare un membro:

    class Foo {
        X m; // compiler error!
    };
    
  • Definisci funzioni o metodi che utilizzano questo tipo

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Utilizzare suoi metodi o campi, infatti tentativo di dereference una variabile con tipo incompleto

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    

Quando si tratta di modelli, non c'è una regola assoluta: se è possibile utilizzare un tipo incompleto come un parametro di template dipende dal modo in cui il tipo viene utilizzato nel modello

.

Per esempio, std::vector<T> richiede il suo parametro di essere un tipo completo, mentre boost::container::vector<T> no. A volte, un tipo completo è richiesta solo se si utilizzano alcune funzioni membro; questo è il caso per std::unique_ptr<T> , per esempio.

Un modello ben documentata deve indicare nella documentazione tutte le esigenze dei suoi parametri, incluso se hanno bisogno di essere tipi completi o meno.

Altri suggerimenti

La regola principale è che si può solo andare avanti-dichiarare le classi la cui memoria il layout (e quindi le funzioni di membro e membri dati) non hanno bisogno di essere conosciuto nel file si inoltra-dichiararlo.

Ciò esclude classi base e qualche cosa ma le classi utilizzate tramite riferimenti e puntatori.

Lakos distingue tra uso di classe

  1. in-nome-only (per i quali una dichiarazione anticipata è sufficiente) e
  2. In-size (per cui è necessario la definizione della classe).

Non ho mai visto è pronunciato più succintamente:)

Così come puntatori e riferimenti a tipi incompleti, si può anche dichiarare i prototipi di funzione che specificano i parametri e / o valori che sono tipi incompleti ritornano. Tuttavia, non è possibile definire di una funzione con un parametro o tipo restituito che è incompleta, a meno che non si tratta di un puntatore o riferimento.

Esempi:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

Nessuna delle risposte finora descritto, quando si può usare una dichiarazione anticipata di un modello di classe. Quindi, qui va.

Un modello di classe può essere inoltrata dichiarato come:

template <typename> struct X;

In seguito la struttura della risposta accettata ,

Ecco cosa si può e non può fare.

Che cosa si può fare con un tipo incompleto:

  • Dichiarare un membro per essere un puntatore o un riferimento al tipo incompleto in un altro modello di classe:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
    
  • dichiarare un membro di un puntatore o un riferimento a una delle sue istanze incomplete:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
    
  • Dichiarare modelli di funzione o modelli funzione membro che accettano / restituiscono tipi incomplete:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
    
  • Dichiarare funzioni o funzioni membro che accettino / restituiscono una delle sue istanze incomplete:

    void      f1(X<int>);
    X<int>    f2();
    
  • Definire modelli di funzione o modelli funzione membro che accettano puntatori / ritorno / riferimenti al tipo incompleto (ma senza usare i suoi membri):

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
    
  • definire funzioni o metodi che accettano puntatori / ritorno / riferimenti a una delle sue istanze incomplete (ma senza usare i suoi membri):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
    
  • usarlo come classe base di un'altra classe template

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
    
  • Si usa per dichiarare un membro di un altro modello di classe:

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
    
  • Definisci modelli di funzione o metodi che utilizzano questo tipo

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }
    

Che cosa non si può fare con un tipo incompleto:

  • Con una delle sue istanze come classe base

    class Foo : X<int> {} // compiler error!
    
  • Con una delle sue istanze per dichiarare un membro:

    class Foo {
        X<int> m; // compiler error!
    };
    
  • Definisci funzioni o metodi che utilizzano una delle sue istanze

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
    
  • usando i metodi o campi di una delle sue istanze, infatti tentativo di dereference una variabile con tipo incompleto

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    
  • Crea istanze esplicite del modello di classe

    template struct X<int>;
    

Nel file in cui si utilizza solo puntatore o riferimento a una classe. E nessuna funzione membro/membro deve essere invocata attraverso quei puntatori/riferimenti.

con class Foo;//dichiarazione anticipata

Possiamo dichiarare membri dati di tipo Foo* o Foo&.

Possiamo dichiarare (ma non definire) funzioni con argomenti e/o valori restituiti di tipo Foo.

Possiamo dichiarare membri dati statici di tipo Foo.Questo perché i membri dati statici sono definiti all'esterno della definizione della classe.

Sto scrivendo questo come una risposta separata piuttosto che come un semplice commento perché non sono d'accordo con la risposta di Luc Touraille, non per motivi di legalità ma per un software robusto e il pericolo di interpretazioni errate.

Nello specifico, ho un problema con il contratto implicito di ciò che ti aspetti che gli utenti della tua interfaccia debbano sapere.

Se stai restituendo o accettando tipi di riferimento, stai semplicemente dicendo che possono passare attraverso un puntatore o un riferimento che a loro volta potrebbero aver conosciuto solo tramite una dichiarazione anticipata.

Quando restituisci un tipo incompleto X f2(); allora stai dicendo il tuo chiamante dovere avere la specifica di tipo completa di X.Ne hanno bisogno per creare l'LHS o l'oggetto temporaneo sul sito della chiamata.

Allo stesso modo, se si accetta un tipo incompleto, il chiamante deve aver costruito l'oggetto che è il parametro.Anche se l'oggetto è stato restituito come un altro tipo incompleto da una funzione, il sito di chiamata necessita della dichiarazione completa.cioè.:

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

Penso che ci sia un principio importante secondo cui un'intestazione dovrebbe fornire informazioni sufficienti per usarla senza una dipendenza che richieda altre intestazioni.Ciò significa che l'intestazione dovrebbe poter essere inclusa in un'unità di compilazione senza causare un errore del compilatore quando si utilizzano le funzioni dichiarate.

Tranne

  1. Se questa dipendenza esterna lo è desiderato comportamento.Invece di usare la compilazione condizionale potresti avere un file Ben documentato requisito per loro di fornire la propria intestazione che dichiara X.Questa è un'alternativa all'uso di #ifdefs e può essere un modo utile per introdurre mock o altre varianti.

  2. La distinzione importante è che alcune tecniche di modello in cui NON sei esplicitamente tenuto a crearne un'istanza, menzionate solo in modo che qualcuno non diventi sarcastico con me.

La regola generale seguo non è quello di includere qualsiasi file di intestazione a meno che non devo. Quindi, a meno che sto memorizzare l'oggetto di una classe come variabile membro della mia classe non voglio includerlo, mi limiterò a utilizzare la dichiarazione in avanti.

Fino a quando non è necessario la definizione (think puntatori e riferimenti) si può ottenere via con le dichiarazioni previsionali. Questo è il motivo per cui la maggior parte si vedrebbe loro in intestazioni mentre i file di implementazione in genere tirerà l'intestazione per la definizione (s) appropriata.

In genere si desidera utilizzare in avanti dichiarazione in un file di intestazione classi quando si desidera utilizzare l'altro tipo (classe) come membro della classe. Non è possibile utilizzare le classi previsionali dichiarato Metodi nel file di intestazione causa C ++ non conosce la definizione di quella classe in quel punto ancora. Ecco la logica si deve spostare nei cpp-files, ma se si utilizza template-funzioni che li dovrebbe ridurre al solo la parte che utilizza il modello e spostare quella funzione nell'intestazione.

Prendere tale dichiarazione in avanti sarà ottenere il codice per compilare (viene creata obj). Il collegamento tuttavia (creazione exe) non sarà successo a meno che non si trovano le definizioni.

Voglio solo aggiungere una cosa importante che si può fare con una classe inoltrato non menzionato nella risposta di Luc Touraille.

Che cosa si può fare con un tipo incompleto:

Definisci funzioni o metodi che accettino / ritorno puntatori / riferimenti al tipo incompleto e in avanti che i puntatori / riferimenti ad un'altra funzione.

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

Un modulo può passare attraverso un oggetto di una classe avanti dichiarata altro modulo.

Come, Luc Touraille ha già spiegato molto bene dove da usare e non usare in avanti dichiarazione della classe.

Vorrei solo aggiungere che il motivo per cui abbiamo bisogno di usarlo.

Dovremmo usare Forward dichiarazione per quanto possibile per evitare l'iniezione di dipendenza indesiderati.

Come file header #include vengono aggiunti su più file, pertanto, se si aggiunge un colpo di testa in un altro file di intestazione si aggiungerà l'iniezione di dipendenza indesiderati in varie parti del codice sorgente che possono essere evitati con l'aggiunta di un'intestazione #include in file .cpp ove possibile, piuttosto che l'aggiunta di un altro file di intestazione e l'uso di classe dichiarazione anticipata ove possibile nei file di intestazione .h.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top