È buona norma fare affidamento sul fatto che le intestazioni vengano incluse in modo transitorio?


37

Sto ripulendo le inclusioni in un progetto C ++ su cui sto lavorando e continuo a chiedermi se dovrei includere esplicitamente tutte le intestazioni utilizzate direttamente in un determinato file o se dovrei includere solo il minimo indispensabile.

Ecco un esempio Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Supponiamo che una dichiarazione a termine per RenderObjectnon sia un'opzione.)

Ora so che RenderObject.hppinclude Texture.hpp- lo so perché ognuno RenderObjectha un Texturemembro. Tuttavia, includo esplicitamente Texture.hppin Entity.hpp, perché non sono sicuro che sia una buona idea fare affidamento sul fatto che sia incluso in RenderObject.hpp.

Quindi: è una buona pratica o no?


19
Dove sono le guardie di inclusione nel tuo esempio? Li hai appena dimenticati per caso, spero?
Doc Brown,

3
Un problema che si verifica quando non si includono tutti i file utilizzati è che a volte l'ordine in cui si includono i file diventa importante. Questo è davvero fastidioso solo nel singolo caso in cui accade, ma a volte le palle di neve e finisci per desiderare davvero che la persona che ha scritto il codice in quel modo venga sfilata davanti a una squadra di fuoco.
Dunk

Questo è il motivo per cui ci sono #ifndef _RENDER_H #define _RENDER_H ... #endif.
sampathsris,

@Dunk Penso che tu abbia frainteso il problema. Con uno dei suoi suggerimenti ciò non dovrebbe accadere.
Mooing Duck

1
@DocBrown, #pragma oncesi deposita, no?
Pacerier,

Risposte:


65

Dovresti sempre includere tutte le intestazioni che definiscono gli oggetti utilizzati in un file .cpp in quel file, indipendentemente da ciò che sai di ciò che è in quei file. Dovresti includere le protezioni in tutti i file di intestazione per assicurarti che includere le intestazioni più volte non sia importante.

Le ragioni:

  • Questo rende chiaro agli sviluppatori che leggono la fonte esattamente ciò che richiede il file sorgente in questione. Qui, qualcuno che osserva le prime righe nel file può vedere che hai a che fare con gli Textureoggetti in questo file.
  • Ciò evita i problemi in cui le intestazioni refactored causano problemi di compilazione quando non richiedono più particolari intestazioni stesse. Ad esempio, supponi di capire che in RenderObject.hpprealtà non ha bisogno di Texture.hppse stesso.

Un corollario è che non dovresti mai includere un'intestazione in un'altra intestazione a meno che non sia esplicitamente necessario in quel file.


10
Concordo con il corollario - con la condizione che dovrebbe SEMPRE includere l'altra intestazione se ne ha bisogno!
Andrew,

1
Non mi piace la pratica di includere direttamente le intestazioni per tutte le singole classi. Sono a favore delle intestazioni cumulative. Cioè, penso che il file di alto livello debba fare riferimento a un "modulo" del tipo che sta usando, ma non è necessario che includa direttamente tutte le singole parti.
edA-qa mort-ora-y

8
Ciò porta a grandi intestazioni monolitiche che ogni file include, anche quando hanno solo bisogno di piccoli frammenti di ciò che contiene, il che porta a lunghi tempi di compilazione e rende più difficile il refactoring.
Gort il robot il

6
Google ha creato uno strumento per aiutarlo a far rispettare esattamente questo consiglio, chiamato include-what-you-use .
Matthew G.

3
Il principale problema di compilazione con grandi intestazioni monolitiche non è il momento di compilare il codice dell'intestazione stesso, ma è necessario compilare ogni file cpp nell'applicazione ogni volta che cambia l'intestazione. Le intestazioni precompilate non lo aiutano.
Gort il robot il

23

La regola generale è: includi ciò che usi. Se si utilizza direttamente un oggetto, includere direttamente il suo file di intestazione. Se usi un oggetto A che usa B ma non usi B da solo, includi solo Ah

Inoltre, mentre ci occupiamo dell'argomento, dovresti includere altri file di intestazione nel file di intestazione solo se ne hai effettivamente bisogno nell'intestazione. Se ne hai solo bisogno in .cpp, includilo solo lì: questa è la differenza tra una dipendenza pubblica e privata e impedirà agli utenti della tua classe di trascinare le intestazioni di cui non hanno davvero bisogno.


10

Continuo a chiedermi se dovrei includere esplicitamente tutte le intestazioni utilizzate direttamente in un determinato file

Sì.

Non si sa mai quando quelle altre intestazioni potrebbero cambiare. Ha senso in tutto il mondo includere, in ogni unità di traduzione, le intestazioni di cui hai bisogno.

Abbiamo protezioni di intestazione per garantire che la doppia inclusione non sia dannosa.


3

Le opinioni differiscono su questo, ma sono del parere che ogni file (sia esso file sorgente c / cpp o file di intestazione h / hpp) debba essere compilato o analizzato da solo.

Pertanto, tutti i file devono includere # tutti i file di intestazione di cui hanno bisogno - non si deve supporre che un file di intestazione sia già stato incluso in precedenza.

È una vera seccatura se devi aggiungere un file di intestazione e scoprire che utilizza un elemento che è definito altrove, senza includerlo direttamente ... quindi devi andare a trovare (e possibilmente finire con quello sbagliato!)

D'altra parte, non importa (come regola generale) se # includi un file che non ti serve ...


Come punto di stile personale, dispongo i file #include in ordine alfabetico, divisi in sistema e applicazione - questo aiuta a rafforzare il messaggio "autonomo e pienamente coerente".


Nota sull'ordine di include: a volte l'ordine è importante, ad esempio quando si includono le intestazioni X11. Ciò può essere dovuto alla progettazione (che in tal caso potrebbe essere considerata una cattiva progettazione), a volte a causa di sfortunati problemi di incompatibilità.
hyde,

Una nota sull'inclusione di intestazioni non necessarie, è importante per i tempi di compilazione, prima direttamente (soprattutto se è un modello C ++ pesante), ma soprattutto quando si includono le intestazioni dello stesso progetto di dipendenza o in cui cambia anche il file include e attiverà la ricompilazione di tutto incluso (se hai dipendenze lavorative, se non lo fai allora devi fare sempre clean build ...).
hyde,

2

Dipende se tale inclusione transitiva è per necessità (ad es. Classe base) o per un dettaglio di implementazione (membro privato).

Per chiarire, l'inclusione transitiva è necessaria quando la si rimuove può essere effettuata solo dopo aver prima modificato le interfacce dichiarate nell'intestazione intermedia. Poiché questa è già una modifica sostanziale, qualsiasi file .cpp che lo utilizza deve essere controllato comunque.

Esempio: Ah è incluso da Bh, utilizzato da C.cpp. Se Bh usasse Ah per alcuni dettagli di implementazione, C.cpp non dovrebbe supporre che Bh continuerà a farlo. Ma se Bh utilizza Ah per una classe di base, C.cpp può presumere che Bh continuerà a includere le intestazioni rilevanti per le sue classi di base.

Vedete qui il vantaggio reale di NON duplicare le inclusioni di intestazione. Supponiamo che la classe di base utilizzata da Bh non appartenga realmente ad Ah e venga trasformata in Bh stesso. Bh è ora un'intestazione autonoma. Se C.cpp includeva ridondante Ah, ora include un'intestazione non necessaria.


2

Può esserci un altro caso: hai Ah, Bh e il tuo C.cpp, Bh include Ah

così in C.cpp, puoi scrivere

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Quindi se non scrivi #include "Ah" qui, cosa può succedere? nel tuo C.cpp, vengono utilizzati sia A che B (ad es. classe). Successivamente hai modificato il tuo codice C.cpp, rimuovi elementi correlati a B, ma lasciando Bh incluso lì.

Se includi sia Ah che Bh e ora a questo punto, gli strumenti che rilevano inclusioni non necessarie potrebbero aiutarti a indicare che l'inclusione di Bh non è più necessaria. Se includi solo Bh come sopra, è difficile per strumenti / umani rilevare l'inclusione non necessaria dopo la modifica del codice.


1

Sto adottando un approccio leggermente diverso dalle risposte proposte.

Nelle intestazioni, includere sempre solo un minimo indispensabile, proprio ciò che è necessario per far passare la compilation. Utilizzare la dichiarazione anticipata ove possibile.

Nei file di origine, non è così importante quanto includi. Le mie preferenze devono ancora includere il minimo per farlo passare.

Per i piccoli progetti, comprese le intestazioni qua e là, non farà differenza. Ma per progetti medio-grandi, può diventare un problema. Anche se per la compilazione viene utilizzato l'hardware più recente, la differenza può essere evidente. Il motivo è che il compilatore deve ancora aprire l'intestazione inclusa e analizzarla. Quindi, per ottimizzare la build, applica la tecnica sopra (includi minimo indispensabile e usa forward dichlare).

Sebbene un po 'datato, il software di progettazione C ++ su larga scala (di John Lakos) spiega tutto questo in dettaglio.


1
Non sono d'accordo con questa strategia ... se includi un file di intestazione in un file di origine, devi quindi rintracciarne tutte le dipendenze. È meglio includere direttamente, piuttosto che provare a documentare l'elenco!
Andrew,

@Andrew ci sono strumenti e script per controllare cosa e quante volte è incluso.
BЈовић

1
Ho notato l'ottimizzazione su alcuni degli ultimi compilatori per far fronte a questo. Riconoscono una tipica dichiarazione di guardia e la elaborano. Quindi, quando # lo includono nuovamente, possono ottimizzare completamente il caricamento del file. Tuttavia, la tua raccomandazione di dichiarazioni anticipate è molto saggia per ridurre il numero di inclusioni. Una volta che inizi a utilizzare le dichiarazioni a termine, diventa un equilibrio tra tempo di esecuzione del compilatore (migliorato dalle dichiarazioni a termine) e facilità d'uso (migliorato da comodi #include extra), che è un equilibrio che ogni azienda imposta in modo diverso.
Cort Ammon,

1
@CortAmmon Un'intestazione tipica include guards, ma il compilatore deve ancora aprirlo e questa è un'operazione lenta
BЈовић

4
@ BЈовић: In realtà, non lo fanno. Tutto quello che devono fare è riconoscere che il file ha protezioni di intestazione "tipiche" e contrassegnarle in modo tale da aprirlo solo una volta. Gcc, ad esempio, ha la documentazione su quando e dove applica questa ottimizzazione: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon,

-4

La buona pratica è quella di non preoccuparsi della tua strategia di intestazione fintanto che si compila.

La sezione header del tuo codice è solo un blocco di righe che nessuno dovrebbe nemmeno guardare fino a quando non ottieni un errore di compilazione facilmente risolvibile. Capisco il desiderio di uno stile "corretto", ma nessuno dei due modi può davvero essere descritto come corretto. Includere un'intestazione per ogni classe è più probabile che causi fastidiosi errori di compilazione basati sull'ordine, ma questi errori di compilazione riflettono anche problemi che potrebbero essere corretti da un'attenta codifica (anche se probabilmente non valgono il tempo per risolvere).

E sì, si dovrà avere quei problemi di ordine basato su una volta che si avvia sempre in friendpaese.

Puoi pensare al problema in due casi.


Caso 1: hai un numero limitato di classi che interagiscono tra loro, diciamo meno di una dozzina. Aggiungete, rimuovete e modificate regolarmente queste intestazioni, in modo tale da influire sulle loro dipendenze. Questo è il caso suggerito dal tuo esempio di codice.

L'insieme delle intestazioni è abbastanza piccolo da non complicare la risoluzione di eventuali problemi. Eventuali problemi difficili vengono risolti riscrivendo una o due intestazioni. Preoccuparsi della tua strategia di intestazione è risolvere i problemi che non esistono.


Caso 2: hai decine di lezioni. Alcune classi rappresentano la spina dorsale del tuo programma e riscrivere le loro intestazioni ti costringerebbe a riscrivere / ricompilare una grande quantità della tua base di codice. Altre classi usano questa spina dorsale per realizzare cose. Ciò rappresenta una tipica impostazione aziendale. Le intestazioni sono distribuite in tutte le directory e non puoi realisticamente ricordare i nomi di tutto.

Soluzione: a questo punto, devi pensare alle tue classi in gruppi logici e raccogliere quei gruppi in intestazioni che ti impediscono di dover #includeripetere ancora e ancora. Non solo questo semplifica la vita, ma è anche un passaggio necessario per sfruttare le intestazioni precompilate .

Ti #includeritrovi in ​​lezioni che non ti servono ma a chi importa ?

In questo caso, il tuo codice sarebbe simile a ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}

13
Ho dovuto -1 questo perché onestamente credo che qualsiasi frase sotto forma di "La buona pratica non è quella di preoccuparsi della tua strategia ____ fintanto che si compila" porta le persone a un cattivo giudizio. Ho scoperto che l'approccio conduce molto rapidamente verso la illeggibilità e la illeggibilità è QUASI tanto brutta quanto "non funziona". Ho anche trovato molte delle principali biblioteche che non sono d'accordo con i risultati di entrambi i casi che descrivi. Ad esempio, Boost fa le intestazioni "collezioni" che consigliate nel caso 2, ma fanno anche molto fornendo le intestazioni classe per classe per quando ne avete bisogno.
Cort Ammon,

3
Ho visto personalmente "non ti preoccupare se si compila" si trasforma in "la nostra applicazione impiega 30 minuti per compilare quando aggiungi un valore a un enum, come diavolo lo risolviamo !?"
Gort il robot il

Ho affrontato il problema del tempo di compilazione nella mia risposta. In effetti, la mia risposta è una delle sole due (nessuna delle quali ha segnato bene) che lo fa. Ma in realtà, questo è tangenziale alla domanda di OP; questo è un "dovrei camel-case i miei nomi variabili?" domanda tipo. Mi rendo conto che la mia risposta è impopolare ma non c'è sempre una buona pratica per tutto, e questo è uno di quei casi.
Domanda

Concordo con il n. 2. Per quanto riguarda le idee precedenti - spero per l'automazione che aggiornerebbe il blocco di intestazione locale - fino ad allora propongo un elenco completo.
chux - Ripristina Monica

L'approccio "includi tutto e il lavello della cucina" potrebbe farti risparmiare un po 'di tempo inizialmente - i tuoi file di intestazione potrebbero persino sembrare più piccoli (poiché la maggior parte delle cose sono incluse indirettamente da .... da qualche parte). Fino a quando non arriverai al punto in cui qualsiasi modifica in qualsiasi luogo provoca una ricompilazione di 30 minuti del tuo progetto. E il tuo completamento automatico IDE-smart fa apparire centinaia di suggerimenti irrilevanti. E accidentalmente confondi due classi con nomi troppo simili o funzioni statiche. E aggiungi una nuova struttura ma poi la compilazione fallisce perché hai una collisione dello spazio dei nomi con una classe totalmente non correlata da qualche parte ...
CharonX
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.