Perché le mie guardie incluse non impediscono l’inclusione ricorsiva e più definizioni di simboli?

Due domande comuni su includono le guardie :

  1. PRIMA DOMANDA:

    Perché non sono incluse le protezioni che proteggono i miei file di intestazione dall’inclusione reciproca e ricorsiva ? Continuo a ricevere errori su simboli non esistenti che sono ovviamente presenti o anche errori di syntax più complessi ogni volta che scrivo qualcosa del tipo:

    “Ah”

    #ifndef A_H #define A_H #include "bh" ... #endif // A_H 

    “Bh”

       #ifndef B_H #define B_H #include "ah" ... #endif // B_H 

      “Main.cpp”

       #include "ah" int main() { ... } 

      Perché ricevo errori nella compilazione di “main.cpp”? Cosa devo fare per risolvere il mio problema?


    1. SECONDA DOMANDA:

      Perché non sono incluse le guardie che impediscono più definizioni ? Ad esempio, quando il mio progetto contiene due file che includono la stessa intestazione, a volte il linker si lamenta del fatto che alcuni simboli vengano definiti più volte. Per esempio:

      “Header.h”

       #ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H 

      “Source1.cpp”

       #include "header.h" ... 

      “Source2.cpp”

       #include "header.h" ... 

      Perché sta succedendo? Cosa devo fare per risolvere il mio problema?

    PRIMA DOMANDA:

    Perché non sono incluse le protezioni che proteggono i miei file di intestazione dall’inclusione reciproca e ricorsiva ?

    Loro sono

    Ciò a cui non stanno aiutando sono le dipendenze tra le definizioni di strutture di dati in intestazioni reciprocamente incluse . Per capire che cosa significa, iniziamo con uno scenario di base e vediamo perché le guardie includono le inclusioni reciproche.

    Supponiamo che i tuoi file di intestazione ah e bh includano contenuti banali, cioè che le ellissi nelle sezioni di codice del testo della domanda vengano sostituite con la stringa vuota. In questa situazione, il tuo main.cpp sarà felicemente compilato. E questo è solo grazie alle tue guardie incluse!

    Se non sei convinto, prova a rimuoverli:

     //================================================ // ah #include "bh" //================================================ // bh #include "ah" //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

    Noterai che il compilatore segnalerà un errore quando raggiunge il limite di profondità di inclusione. Questo limite è specifico per l’implementazione. Per paragrafo 16.2 / 6 della norma C ++ 11:

    Una direttiva #include di pre-elaborazione può apparire in un file sorgente che è stato letto a causa di una direttiva #include in un altro file, fino a un limite di nidificazione definito dall’implementazione .

    Quindi cosa sta succedendo ?

    1. Quando si analizza main.cpp , il preprocessore incontrerà la direttiva #include "ah" . Questa direttiva dice al preprocessore di elaborare il file di intestazione ah , prendere il risultato di tale elaborazione e sostituire la stringa #include "ah" con quel risultato;
    2. Durante l’elaborazione di ah , il preprocessore incontrerà la direttiva #include "bh" , e si applica lo stesso meccanismo: il preprocessore elaborerà il file di intestazione bh , prenderà il risultato della sua elaborazione e sostituirà la direttiva #include con quel risultato;
    3. Durante l’elaborazione di bh , la direttiva #include "ah" dirà al preprocessore di elaborare ah e sostituire quella direttiva con il risultato;
    4. Il preprocessore inizierà nuovamente l’analisi di ah , incontrerà di nuovo la direttiva #include "bh" e questo imposterà un processo ricorsivo potenzialmente infinito. Quando si raggiunge il livello di annidamento critico, il compilatore segnala un errore.

    Quando sono presenti protezioni incluse , tuttavia, nessuna ricorsione infinita verrà impostata nel passaggio 4. Vediamo perché:

    1. ( come prima ) Quando si analizza main.cpp , il preprocessore incontrerà la direttiva #include "ah" . Questo dice al preprocessore di elaborare il file di intestazione ah , prendere il risultato di tale elaborazione e sostituire la stringa #include "ah" con quel risultato;
    2. Durante l’elaborazione di ah , il preprocessore incontrerà la direttiva #ifndef A_H . Poiché la macro A_H non è stata ancora definita, continuerà a elaborare il seguente testo. La direttiva successiva ( #defines A_H ) definisce la macro A_H . Quindi, il preprocessore incontrerà la direttiva #include "bh" : il preprocessore elaborerà ora il file di intestazione bh , prenderà il risultato della sua elaborazione e sostituirà la direttiva #include con quel risultato;
    3. Durante l’elaborazione di bh , il preprocessore incontrerà la direttiva #ifndef B_H . Poiché la macro B_H non è stata ancora definita, continuerà a elaborare il seguente testo. La direttiva successiva ( #defines B_H ) definisce la macro B_H . Quindi, la direttiva #include "ah" dirà al preprocessore di elaborare ah e sostituire la direttiva #include in bh con il risultato della pre-elaborazione ah ;
    4. Il compilatore inizierà nuovamente la pre-elaborazione e incontrerà nuovamente la direttiva #ifndef A_H . Tuttavia, durante la preelaborazione precedente, è stata definita la macro A_H . Pertanto, il compilatore salterà questa volta il seguente testo finché non verrà trovata la direttiva #endif corrispondente e l’output di questa elaborazione è la stringa vuota (supponendo che nulla segua la direttiva #endif , ovviamente). Il preprocessore sostituirà quindi la direttiva #include "ah" in bh con la stringa vuota e ripercorrerà l’esecuzione fino a sostituire la direttiva #include originale in main.cpp .

    Quindi, includere le guardie proteggono contro l’inclusione reciproca . Tuttavia, non possono aiutare con le dipendenze tra le definizioni delle tue classi in file inclusi reciprocamente:

     //================================================ // ah #ifndef A_H #define A_H #include "bh" struct A { }; #endif // A_H //================================================ // bh #ifndef B_H #define B_H #include "ah" struct B { A* pA; }; #endif // B_H //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

    Date le intestazioni di cui sopra, main.cpp non verrà compilato.

    Perché sta succedendo?

    Per vedere cosa sta succedendo, è sufficiente ripetere i passaggi 1-4.

    È facile vedere che i primi tre passaggi e la maggior parte del quarto passaggio non sono influenzati da questo cambiamento (basta leggerli per essere convinti). Tuttavia, qualcosa di diverso accade alla fine del passo 4: dopo aver sostituito la direttiva #include "ah" in bh con la stringa vuota, il preprocessore inizierà l’analisi del contenuto di bh e, in particolare, la definizione di B Sfortunatamente, la definizione di B menziona la class A , che non è mai stata soddisfatta prima esattamente a causa delle guardie di inclusione!

    Dichiarare una variabile membro di un tipo che non è stato dichiarato in precedenza è, naturalmente, un errore, e il compilatore lo indicherà gentilmente.

    Cosa devo fare per risolvere il mio problema?

    Hai bisogno di dichiarazioni anticipate .

    In effetti, la definizione della class A non è necessaria per definire la class B , poiché un puntatore a A viene dichiarato come variabile membro e non un object di tipo A Poiché i puntatori hanno dimensioni fisse, il compilatore non avrà bisogno di conoscere il layout esatto di A né di calcolare le sue dimensioni per definire correttamente la class B Quindi, è sufficiente inoltrare la class A in bh e rendere il compilatore consapevole della sua esistenza:

     //================================================ // bh #ifndef B_H #define B_H // Forward declaration of A: no need to #include "ah" struct A; struct B { A* pA; }; #endif // B_H 

    Il tuo main.cpp ora compilerà sicuramente. Un paio di osservazioni:

    1. Non solo rompere l’inclusione reciproca sostituendo la direttiva #include con una dichiarazione bh in bh era sufficiente per esprimere efficacemente la dipendenza di B su A : utilizzare le dichiarazioni anticipate ogni qualvolta ansible / pratico è anche considerato una buona pratica di programmazione , perché aiuta evitando inutili inclusioni, riducendo così il tempo complessivo di compilazione. Tuttavia, dopo aver eliminato l’inclusione reciproca, main.cpp dovrà essere modificato in #include sia ah che bh (se quest’ultimo è necessario), perché bh non è indirettamente #include d attraverso ah ;
    2. Mentre una dichiarazione anticipata di class A è sufficiente per il compilatore per dichiarare i puntatori a quella class (o per usarla in qualsiasi altro contesto in cui i tipi incompleti sono accettabili), i puntatori di dereferenziamento ad A (ad esempio per richiamare una funzione membro) o calcolare il suo le dimensioni sono operazioni illegali su tipi incompleti: se necessario, è necessario che la definizione completa di A sia disponibile per il compilatore, il che significa che deve essere incluso il file di intestazione che lo definisce. Questo è il motivo per cui le definizioni delle classi e l’implementazione delle loro funzioni membro sono solitamente suddivise in un file di intestazione e un file di implementazione per quella class (i modelli di class sono un’eccezione a questa regola): i file di implementazione, che non sono mai #include d da altri file in il progetto, può tranquillamente #include tutte le intestazioni necessarie per rendere visibili le definizioni. I file di intestazione, d’altro canto, non #include altri file di intestazione, a meno che non abbiano realmente bisogno di farlo (ad esempio, per rendere visibile la definizione di una class base ) e utilizzeranno le dichiarazioni anticipate ogni volta che è ansible / pratico.

    SECONDA DOMANDA:

    Perché non sono incluse le guardie che impediscono più definizioni ?

    Loro sono

    Ciò che non ti proteggono da è più definizioni in unità di traduzione separate . Questo è anche spiegato in questo Q & A su StackOverflow.

    source1.cpp , prova a rimuovere le protezioni di inclusione e a compilare la seguente versione modificata di source1.cpp (o source2.cpp , per quello che conta):

     //================================================ // source1.cpp // // Good luck getting this to compile... #include "header.h" #include "header.h" int main() { ... } 

    Il compilatore si lamenterà sicuramente della ridefinizione di f() . È ovvio: la sua definizione viene inclusa due volte! Tuttavia, il precedente source1.cpp verrà compilato senza problemi quando header.h contiene le protezioni di inclusione appropriate . Questo è previsto.

    Tuttavia, anche quando sono presenti le protezioni di inclusione e il compilatore smetterà di disturbarti con un messaggio di errore, il linker insisterà sul fatto che vengano trovate più definizioni durante l’unione del codice object ottenuto dalla compilazione di source1.cpp e source2.cpp , e si rifiuterà di generare il tuo eseguibile.

    Perché sta succedendo?

    Fondamentalmente, ogni file .cpp (il termine tecnico in questo contesto è l’ unità di traduzione ) nel tuo progetto è compilato separatamente e indipendentemente . Quando si analizza un file .cpp , il preprocessore elabora tutte le direttive #include ed espande tutte le invocazioni di macro che incontra, e l’output di questa pura elaborazione di testo verrà fornito in input al compilatore per la sua traduzione in codice object. Una volta che il compilatore ha terminato di produrre il codice object per un’unità di traduzione, procederà con il successivo, e tutte le definizioni di macro che sono state incontrate durante l’elaborazione della precedente unità di traduzione saranno dimenticate.

    In effetti, la compilazione di un progetto con n unità di traduzione (file .cpp ) è come eseguire lo stesso programma (il compilatore) n volte, ogni volta con un input diverso: esecuzioni diverse dello stesso programma non condivideranno lo stato del precedente esecuzione (i) del programma . Quindi, ogni traduzione viene eseguita indipendentemente e i simboli del preprocessore incontrati durante la compilazione di un’unità di traduzione non verranno ricordati quando si compilano altre unità di traduzione (se ci pensate per un momento, vi renderete conto facilmente che questo è in realtà un comportamento desiderabile).

    Pertanto, anche se le protezioni incluse aiutano a prevenire inclusioni reciproche ricorsive e inclusioni ridondanti della stessa intestazione in un’unità di traduzione, non possono rilevare se la stessa definizione sia inclusa in unità di traduzione diverse .

    Tuttavia, quando si unisce il codice object generato dalla compilazione di tutti i file .cpp del progetto, il linker vedrà che lo stesso simbolo viene definito più di una volta e poiché viola la regola della definizione unica . Per paragrafo 3.2 / 3 della norma C ++ 11:

    Ogni programma deve contenere esattamente una definizione di ogni funzione o variabile non in-linea che è odr-usata in quel programma; nessuna diagnostica richiesta. La definizione può apparire esplicitamente nel programma, può essere trovata nello standard o in una libreria definita dall’utente o (quando appropriato) è definita implicitamente (vedere 12.1, 12.4 e 12.8). Una funzione inline deve essere definita in ogni unità di traduzione in cui è odr-used .

    Quindi, il linker emetterà un errore e si rifiuterà di generare l’eseguibile del tuo programma.

    Cosa devo fare per risolvere il mio problema?

    Se si desidera mantenere la definizione della propria funzione in un file di intestazione che #include d da più unità di traduzione (si noti che non si verificherà alcun problema se l’intestazione contiene #include solo una unità di traduzione), è necessario utilizzare la parola chiave inline .

    Altrimenti, devi mantenere solo la dichiarazione della tua funzione in header.h , inserendo la sua definizione (corpo) in un solo file .cpp separato (questo è l’approccio classico).

    La parola chiave inline rappresenta una richiesta non vincolante al compilatore per incorporare il corpo della funzione direttamente sul sito di chiamata, piuttosto che impostare uno stack frame per una normale chiamata di funzione. Sebbene il compilatore non debba soddisfare la tua richiesta, la parola chiave inline riesce a dire al linker di tollerare più definizioni di simboli. Secondo l’articolo 3.2 / 5 della norma C ++ 11:

    Possono esserci più di una definizione di un tipo di class (Clausola 9), tipo di enumerazione (7.2), funzione inline con collegamento esterno (7.1.2), modello di class (Clausola 14), modello di funzione non statico (14.5.6) , membro dati statici di un modello di class (14.5.1.3), funzione membro di un modello di class (14.5.1.1) o specializzazione del modello per la quale alcuni parametri modello non sono specificati (14.7, 14.5.5) in un programma a condizione che ciascuno la definizione appare in una diversa unità di traduzione e a condizione che le definizioni soddisfino i seguenti requisiti […]

    Il paragrafo precedente elenca sostanzialmente tutte le definizioni che vengono comunemente inserite nei file di intestazione , perché possono essere tranquillamente incluse in più unità di traduzione. Tutte le altre definizioni con collegamento esterno, invece, appartengono ai file sorgente.

    L’utilizzo della parola chiave static anziché della parola chiave inline determina anche la soppressione degli errori del linker fornendo il collegamento interno della funzione, rendendo così ogni unità di traduzione in possesso di una copia privata di tale funzione (e delle sue variabili statiche locali). Tuttavia, questo alla fine si traduce in un eseguibile più grande, e l’uso di inline dovrebbe essere preferito in generale.

    Un modo alternativo per ottenere lo stesso risultato della parola chiave static è inserire la funzione f() in uno spazio dei nomi senza nome . Per paragrafo 3.5 / 4 dello standard C ++ 11:

    Uno spazio dei nomi senza nome o uno spazio dei nomi dichiarato direttamente o indirettamente in uno spazio dei nomi senza nome ha un collegamento interno. Tutti gli altri spazi dei nomi hanno un collegamento esterno. Un nome con scope namespace a cui non è stato dato il collegamento interno sopra ha lo stesso linkage dello spazio dei nomi che lo racchiude se è il nome di:

    – una variabile; o

    una funzione ; o

    – una class denominata (clausola 9) o una class senza nome definita in una dichiarazione typedef in cui la class ha il nome typedef ai fini del collegamento (7.1.3); o

    – un’enumerazione con nome (7.2) o un’enumerazione senza nome definita in una dichiarazione typedef in cui l’enumerazione ha il nome typedef ai fini del collegamento (7.1.3); o

    – un enumeratore appartenente a un’enumerazione con linkage; o

    – Un modello.

    Per la stessa ragione sopra menzionata, la parola chiave inline dovrebbe essere preferita.