Quando posso usare una dichiarazione anticipata?


602

Sto cercando la definizione di quando mi è permesso fare una dichiarazione in avanti di una classe nel file di intestazione di un'altra classe:

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


14
Voglio disperatamente che questo venga rinominato "quando dovrei " e le risposte aggiornate in modo appropriato ...
deworde

12
@deworde Quando dici quando "dovresti" stai chiedendo un parere.
AturSams,

@deworde è mia comprensione che si desidera utilizzare dichiarazioni in avanti ogni volta che è possibile, per migliorare i tempi di costruzione ed evitare riferimenti circolari. L'unica eccezione che mi viene in mente è quando un file include contiene typedef, nel qual caso c'è un compromesso tra la ridefinizione del typedef (e il rischio di cambiarlo) e l'inclusione di un intero file (insieme alle sue inclusioni ricorsive).
Ohad Schneider,

@OhadSchneider Da una prospettiva pratica, non sono un grande fan delle intestazioni che il mio. ÷
deworde

fondamentalmente richiede sempre di includere un'intestazione diversa per usarli (il decl forward del parametro del costruttore è un grande colpevole qui)
deworde

Risposte:


962

Mettiti nella posizione del compilatore: quando dichiari in avanti un tipo, tutto il compilatore sa che esiste questo tipo; non sa nulla delle sue dimensioni, membri o metodi. Questo è il motivo per cui si chiama un tipo incompleto . Pertanto, non è possibile utilizzare il tipo per dichiarare un membro o una classe base, poiché il compilatore dovrebbe conoscere il layout del tipo.

Supponendo la seguente dichiarazione diretta.

class X;

Ecco cosa puoi e non puoi fare.

Cosa puoi fare con un tipo incompleto:

  • Dichiarare un membro come puntatore o riferimento al tipo incompleto:

    class Foo {
        X *p;
        X &r;
    };
  • Dichiarare funzioni o metodi che accettano / restituiscono tipi incompleti:

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

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

Cosa non puoi fare con un tipo incompleto:

  • Usalo come classe di base

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

    class Foo {
        X m; // compiler error!
    };
  • Definire funzioni o metodi usando questo tipo

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Usa i suoi metodi o campi, infatti cerca di dereferenziare 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 esiste una regola assoluta: se è possibile utilizzare un tipo incompleto come parametro del modello dipende dal modo in cui il tipo viene utilizzato nel modello.

Ad esempio, std::vector<T>richiede che il suo parametro sia un tipo completo, mentre boost::container::vector<T>non lo è. A volte, è richiesto un tipo completo solo se si utilizzano determinate funzioni membro; questo è il casostd::unique_ptr<T> , ad esempio.

Un modello ben documentato dovrebbe indicare nella sua documentazione tutti i requisiti dei suoi parametri, incluso se devono essere tipi completi o meno.


4
Ottima risposta, ma per favore vedi la mia sotto per il punto di ingegneria su cui non sono d'accordo. In breve, se non includi le intestazioni per tipi incompleti che accetti o restituisci, imponi una dipendenza invisibile al consumatore che deve sapere quali altri hanno bisogno.
Andy Dent,

2
@AndyDent: Vero, ma il consumatore dell'intestazione deve solo includere le dipendenze che utilizza effettivamente, quindi questo segue il principio C ++ di "paghi solo per quello che usi". Ma in effetti, può essere scomodo per l'utente che si aspetterebbe che l'intestazione fosse autonoma.
Luc Touraille,

8
Questo insieme di regole ignora un caso molto importante: è necessario un tipo completo per creare un'istanza della maggior parte dei modelli nella libreria standard. È necessario prestare particolare attenzione a questo, poiché la violazione della regola comporta un comportamento indefinito e potrebbe non causare un errore del compilatore.
James Kanze,

12
+1 per "mettiti nella posizione del compilatore". Immagino che il "compilatore" abbia i baffi.
PascalVKooten,

3
@JesusChrist: Esatto: quando si passa un oggetto in base al valore, il compilatore deve conoscerne le dimensioni per effettuare la manipolazione dello stack appropriata; quando passa un puntatore o un riferimento, il compilatore non ha bisogno della dimensione o del layout dell'oggetto, ma solo della dimensione di un indirizzo (cioè la dimensione di un puntatore), che non dipende dal tipo indicato.
Luc Touraille,

45

La regola principale è che è possibile dichiarare in avanti solo le classi il cui layout di memoria (e quindi le funzioni membro e i membri dei dati) non devono essere conosciute nel file che si dichiara in avanti.

Ciò escluderebbe le classi base e tutto tranne le classi utilizzate tramite riferimenti e puntatori.


6
Quasi. È anche possibile fare riferimento a tipi incompleti "semplici" (ovvero senza puntatore / riferimento) come parametri o tipi restituiti nei prototipi di funzioni.
j_random_hacker,

Che dire delle classi che voglio usare come membri di una classe che definisco nel file di intestazione? Posso inoltrarli dichiarandoli?
Igor Oks,

1
Sì, ma in tal caso è possibile utilizzare solo un riferimento o un puntatore alla classe dichiarata in avanti. Tuttavia, ti consente di avere membri.
Riunione del

32

Lakos distingue tra uso di classe

  1. solo in nome (per il quale è sufficiente una dichiarazione anticipata) e
  2. di dimensioni (per cui è necessaria la definizione di classe).

Non l'ho mai visto pronunciato in modo più succinto :)


2
Cosa significa solo in-name?
Boon il

4
@Boon: oso dirlo ...? Se usi solo il nome della classe ?
Marc Mutz - mmutz,

1
Più uno per Lakos, Marc
mlvljr,

28

Oltre a puntatori e riferimenti a tipi incompleti, è anche possibile dichiarare prototipi di funzioni che specificano parametri e / o valori di ritorno che sono tipi incompleti. Tuttavia, non è possibile definire una funzione con un parametro o un tipo restituito incompleta, a meno che non sia un puntatore o un 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

19

Nessuna delle risposte finora descrive quando si può usare una dichiarazione diretta di un modello di classe. Quindi, eccola qui.

Un modello di classe può essere inoltrato dichiarato come:

template <typename> struct X;

Seguendo la struttura della risposta accettata ,

Ecco cosa puoi e non puoi fare.

Cosa puoi fare con un tipo incompleto:

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

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

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

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

    void      f1(X<int>);
    X<int>    f2();
  • Definire modelli di funzione o modelli di funzioni membro che accettano / restituiscono puntatori / 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 / restituiscono puntatori / 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; }
  • Usalo come classe base di un'altra classe modello

    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.
  • Usalo 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.
  • Definire modelli o metodi di funzione usando 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
    }

Cosa non puoi fare con un tipo incompleto:

  • Usa una delle sue istanze come classe base

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

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

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Usa i metodi o i campi di una delle sue istanze, cercando infatti di dereferenziare 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>;

2
"Nessuna delle risposte finora descrive quando si può inoltrare la dichiarazione di un modello di classe." Non è semplicemente perché la semantica di Xe X<int>sono esattamente gli stessi, e solo la sintassi di dichiarazione in avanti differisce in qualche modo sostanziale, con tutte le righe tranne 1 che equivalgono a prendere solo quella di Luc e s/X/X<int>/g? È davvero necessario? O ho perso un piccolo dettaglio diverso? È possibile, ma ho confrontato visivamente alcune volte e non riesco a vedere alcun ...
underscore_d

Grazie! Questa modifica aggiunge una tonnellata di informazioni preziose. Dovrò leggerlo più volte per comprenderlo appieno ... o forse usare la tattica spesso migliore di aspettare fino a quando non sarò orribilmente confuso in codice reale e tornerò qui! Sospetto che sarò in grado di utilizzare questo per ridurre le dipendenze in vari luoghi.
underscore_d

4

Nel file in cui si utilizza solo il puntatore o il riferimento a una classe. E nessuna funzione membro / membro deve essere invocata se si considera quel puntatore / riferimento.

con class Foo;// dichiarazione a termine

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

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

Possiamo dichiarare membri di dati statici di tipo Foo. Questo perché i membri di dati statici sono definiti al di fuori della definizione di classe.


4

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

In particolare, 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 potrebbero a loro volta conoscere solo attraverso una dichiarazione diretta.

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

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

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 richiede altre intestazioni. Ciò significa che l'intestazione dovrebbe essere in grado di essere inclusa in un'unità di compilazione senza causare un errore del compilatore quando si utilizzano le funzioni dichiarate.

tranne

  1. Se si desidera questa dipendenza esterna comportamento. Invece di usare la compilazione condizionale potresti avere un requisito ben documentato per loro di fornire la propria intestazione che dichiara X. Questa è un'alternativa all'utilizzo di #ifdefs e può essere un modo utile per introdurre simulazioni o altre varianti.

  2. L'importante distinzione sono alcune tecniche di template in cui NON ci si aspetta esplicitamente di istanziarle, menzionate solo in modo che qualcuno non si sbrighi con me.


"Penso che ci sia un principio importante secondo cui un'intestazione dovrebbe fornire informazioni sufficienti per usarla senza una dipendenza che richiede altre intestazioni". - un altro problema è menzionato in un commento di Adrian McCarthy sulla risposta di Naveen. Ciò fornisce una valida ragione per non seguire il principio "dovrebbe fornire informazioni sufficienti per l'uso" anche per i tipi attualmente non basati su modelli.
Tony Delroy,

3
Stai parlando di quando dovresti (o non dovresti) usare la dichiarazione diretta. Non è assolutamente questo il punto di questa domanda. Si tratta di conoscere le possibilità tecniche quando (ad esempio) vogliono rompere un problema di dipendenza circolare.
JonnyJD

1
I disagree with Luc Touraille's answerQuindi scrivigli un commento, incluso un link a un post sul blog se ne hai bisogno. Questo non risponde alla domanda posta. Se tutti pensassero a domande su come X funzioni risposte giustificate in disaccordo con X nel farlo o discutendo i limiti entro i quali dovremmo limitare la nostra libertà di usare X - non avremmo quasi nessuna risposta reale.
underscore_d

3

La regola generale che seguo è di non includere alcun file di intestazione a meno che non sia necessario. Quindi, a meno che non stia memorizzando l'oggetto di una classe come variabile membro della mia classe, non la includerò, userò semplicemente la dichiarazione forward.


2
Ciò interrompe l'incapsulamento e rende il codice fragile. Per fare ciò, devi sapere se il tipo è un typedef o una classe per un modello di classe con parametri di modello predefiniti e se l'implementazione cambia, dovrai aggiornare ogni volta che hai usato una dichiarazione diretta.
Adrian McCarthy,

@AdrianMcCarthy ha ragione, e una soluzione ragionevole è quella di avere un'intestazione di dichiarazione in avanti inclusa nell'intestazione di cui dichiara il contenuto in avanti, che dovrebbe essere di proprietà / mantenuto / spedito da chiunque possieda anche quell'intestazione. Ad esempio: l'intestazione della libreria standard iosfwd, che contiene dichiarazioni in avanti del contenuto di iostream.
Tony Delroy

3

Finché non hai bisogno della definizione (pensa a puntatori e riferimenti) puoi farla franca con le dichiarazioni in avanti. Questo è il motivo per cui per lo più li vedresti nelle intestazioni mentre i file di implementazione in genere tirano l'intestazione per le definizioni appropriate.


0

In genere si desidera utilizzare la dichiarazione diretta in un file di intestazione delle classi quando si desidera utilizzare l'altro tipo (classe) come membro della classe. Non è possibile utilizzare i metodi delle classi dichiarate in avanti nel file di intestazione perché C ++ non conosce ancora la definizione di quella classe in quel punto. Questa è la logica che devi spostare nei file .cpp, ma se stai usando le funzioni template dovresti ridurle solo alla parte che usa il template e spostare quella funzione nell'intestazione.


Questo non ha senso. Non si può avere un membro di un tipo incompleto. La dichiarazione di qualsiasi classe deve fornire tutto ciò che gli utenti devono sapere sulla sua dimensione e layout. Le sue dimensioni includono le dimensioni di tutti i suoi membri non statici. La dichiarazione anticipata di un membro non lascia agli utenti alcuna idea delle sue dimensioni.
underscore_d,

0

Supponi che la dichiarazione in avanti faccia compilare il tuo codice (viene creato obj). Il collegamento tuttavia (creazione exe) non avrà esito positivo a meno che non vengano trovate le definizioni.


2
Perché mai 2 persone hanno votato questo? Non stai parlando di cosa stia parlando la domanda. Intendi normale - non forward - dichiarazione di funzioni . La domanda riguarda la dichiarazione anticipata delle classi . Come hai detto "la dichiarazione in avanti farà compilare il tuo codice", fammi un favore: compila class A; class B { A a; }; int main(){}e fammi sapere come va. Ovviamente non verrà compilato. Tutte le risposte appropriate qui spiegano perché e i contesti precisi e limitati in cui è valida la dichiarazione a termine . Invece hai scritto questo su qualcosa di totalmente diverso.
underscore_d,

0

Voglio solo aggiungere una cosa importante che puoi fare con una classe inoltrata non menzionata nella risposta di Luc Touraille.

Cosa puoi fare con un tipo incompleto:

Definire funzioni o metodi che accettano / restituiscono puntatori / riferimenti al tipo incompleto e inoltrano tali puntatori / riferimenti a 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 dichiarata diretta ad un altro modulo.


"una classe inoltrata" e "una classe dichiarata in avanti" potrebbero essere scambiate per riferirsi a due cose molto diverse. Quello che hai scritto segue direttamente dai concetti impliciti nella risposta di Luc, quindi mentre avrebbe fatto un buon commento aggiungendo chiarimenti espliciti, non sono sicuro che giustifichi una risposta.
underscore_d

0

Come, Luc Touraille ha già spiegato molto bene dove usare e non usare la dichiarazione anticipata della classe.

Aggiungerò semplicemente a questo perché dobbiamo usarlo.

Dovremmo usare la dichiarazione diretta ove possibile per evitare l'iniezione di dipendenza indesiderata.

Poiché i #includefile di intestazione vengono aggiunti su più file, quindi, se si aggiunge un'intestazione in un altro file di intestazione, si aggiungerà un'iniezione di dipendenza indesiderata in varie parti del codice sorgente che può essere evitata aggiungendo l' #includeintestazione nei .cppfile ove possibile anziché aggiungendo a un altro file di intestazione e utilizzare la dichiarazione forward della classe ove possibile nei .hfile di intestazione .

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.