Quando posso usare una dichiarazione in avanti?

Sto cercando la definizione di quando sono autorizzato a fare una dichiarazione anticipata di una class nel file di intestazione di un’altra class:

Sono autorizzato a farlo per una class base, per una class tenuta come membro, per una class passata alla funzione membro per riferimento, ecc.?

Mettiti nella posizione del compilatore: quando inoltrerai un tipo, tutto il compilatore sa che questo tipo esiste; non sa nulla delle sue dimensioni, membri o metodi. Questo è il motivo per cui è chiamato un tipo incompleto . Pertanto, non è ansible utilizzare il tipo per dichiarare un membro o una class base, poiché il compilatore dovrebbe conoscere il layout del tipo.

Supponendo la seguente dichiarazione anticipata.

class X; 

Ecco cosa puoi e cosa non puoi fare.

Cosa puoi fare con un tipo incompleto:

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

     class Foo { X *pt; X &pt; }; 
  • 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 utilizzare i suoi membri):

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

Cosa non puoi fare con un tipo incompleto:

  • Usalo come una class base

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

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

     void f1(X x) {} // compiler error! X f2() {} // compiler error! 
  • Usa i suoi metodi o campi, in effetti cercando 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 è ansible utilizzare un tipo incompleto come parametro modello dipende dal modo in cui il tipo viene utilizzato nel modello.

Ad esempio, std::vector richiede che il suo parametro sia di tipo completo, mentre boost::container::vector non lo fa. A volte, un tipo completo è richiesto solo se si utilizzano determinate funzioni membro; questo è il caso per std::unique_ptr , per esempio.

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

La regola principale è che è ansible inoltrare solo le classi il cui layout di memoria (e quindi le funzioni membro e i membri dati) non devono essere conosciuti nel file inoltrato – dichiararlo.

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

Lakos distingue tra l’uso della class

  1. solo nome (per il quale una dichiarazione anticipata è sufficiente) e
  2. in-size (per cui è necessaria la definizione della class).

Non l’ho mai visto pronunciato più sinteticamente 🙂

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

Finora nessuna delle risposte descrive quando è ansible utilizzare una dichiarazione anticipata di un modello di class. Quindi, ecco qui.

Un modello di class può essere inoltrato dichiarato come:

 template  struct X; 

Seguendo la struttura della risposta accettata ,

Ecco cosa puoi e cosa 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 class:

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

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

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

     void f1(X); X f2(); 
  • Definisci modelli di funzioni o modelli di funzioni membro che accettano / restituiscono puntatori / riferimenti al tipo incompleto (ma senza utilizzare i suoi membri):

     template  void f3(X*, X&) {} template  X& f4(X& in) { return in; } template  X* f5(X* in) { return in; } 
  • Definire funzioni o metodi che accettano / restituiscono puntatori / riferimenti a una delle sue istanze incomplete (ma senza utilizzare i suoi membri):

     void f3(X*, X&) {} X& f4(X& in) { return in; } X* f5(X* in) { return in; } 
  • Usalo come una class base di un’altra class template

     template  class Foo : X {} // OK as long as X is defined before // Foo is instantiated. Foo a1; // Compiler error. template  struct X {}; Foo a2; // OK since X is now defined. 
  • Usalo per dichiarare un membro di un altro modello di class:

     template  class Foo { X m; // OK as long as X is defined before // Foo is instantiated. }; Foo a1; // Compiler error. template  struct X {}; Foo a2; // OK since X is now defined. 
  • Definisci modelli di funzioni o metodi utilizzando questo tipo

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

Cosa non puoi fare con un tipo incompleto:

  • Utilizzare una delle sue istanziazioni come class base

     class Foo : X {} // compiler error! 
  • Utilizzare una delle sue istanze per dichiarare un membro:

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

     void f1(X x) {} // compiler error! X f2() {return X(); } // compiler error! 
  • Utilizzare i metodi oi campi di una delle sue istanziazioni, in effetti tentando di dereferenziare una variabile con tipo incompleto

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

     template struct X; 

Nel file in cui si utilizza solo il puntatore o il riferimento a una class.E nessuna funzione membro / membro deve essere invocata, pensate a quel puntatore / riferimento.

con class Foo; // dichiarazione anticipata

Possiamo dichiarare membri di 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 dei dati statici sono definiti al di fuori della definizione della class.

Finché non hai bisogno della definizione (pensa puntatori e riferimenti) puoi farcela con le dichiarazioni in avanti. Questo è il motivo per cui le vedrai principalmente nelle intestazioni mentre i file di implementazione tipicamente estraggono l’intestazione per le definizioni appropriate.

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

Sto scrivendo questo come una risposta separata piuttosto che un semplice commento perché non sono d’accordo con la risposta di Luc Touraille, non sulla base della legalità, ma per un software robusto e il pericolo di un’interpretazione errata.

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 i tipi di riferimento, allora stai solo dicendo che possono passare attraverso un puntatore o un riferimento che a loro volta potrebbero conoscere solo attraverso una dichiarazione anticipata.

Quando si restituisce un tipo incompleto X f2(); quindi stai dicendo che il tuo chiamante deve avere la specifica completa di tipo di X. Ne hanno bisogno per creare l’LHS o l’object temporaneo nel sito di chiamata.

Allo stesso modo, se si accetta un tipo incompleto, il chiamante deve aver costruito l’object che è il parametro. Anche se quell’object è 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 che un’intestazione dovrebbe fornire informazioni sufficienti per usarla senza una dipendenza che richiede altre intestazioni. Ciò significa che l’intestazione dovrebbe poter essere inclusa in una unità di compilazione senza causare un errore del compilatore quando si utilizzano le funzioni dichiarate.

tranne

  1. Se questa dipendenza esterna è il comportamento desiderato . Invece di usare la compilazione condizionale, potresti avere un requisito ben documentato per 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. L’importante distinzione sono alcune tecniche di template in cui NON ci si aspetta esplicitamente di istanziarle, menzionate solo così che qualcuno non si arrabbi con me.

In genere, si desidera utilizzare la dichiarazione diretta in un file di intestazione delle classi quando si desidera utilizzare l’altro tipo (class) come membro della class. Non è ansible utilizzare i metodi delle classi forward-dichiarati nel file di intestazione perché C ++ non conosce ancora la definizione di quella class a quel punto. È logico che devi spostarti nei file .cpp, ma se utilizzi le funzioni dei modelli devi ridurli solo alla parte che utilizza il modello e spostare tale funzione nell’intestazione.

Prendilo, quella dichiarazione in avanti farà compilare il tuo codice (l’object è stato creato). Il collegamento tuttavia (creazione exe) non avrà esito positivo a meno che non vengano trovate le definizioni.

Voglio solo aggiungere una cosa importante che puoi fare con una class 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 i 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 object di una class dichiarata inoltrata a un altro modulo.