È buona prassi affidarsi a intestazioni incluse in via transitoria?

34

Sto ripulendo gli include in un progetto C ++ su cui sto lavorando, e continuo a chiedermi se dovrei includere esplicitamente tutte le intestazioni usate direttamente in un particolare 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 in avanti per RenderObject non sia un'opzione.)

Ora, so che RenderObject.hpp include Texture.hpp - lo so perché ogni RenderObject ha un membro Texture . Tuttavia, includo esplicitamente Texture.hpp in Entity.hpp , perché non sono sicuro che sia una buona idea affidarsi ad esso in RenderObject.hpp .

Quindi: è una buona pratica o no?

    
posta futlib 07.11.2014 - 05:31
fonte

8 risposte

61

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

I motivi:

  • Ciò rende chiaro agli sviluppatori che leggono la fonte esattamente ciò che richiede il file sorgente in questione. Qui, qualcuno che guarda le prime righe del file può vedere che stai trattando con Texture oggetti in questo file.
  • Questo evita problemi in cui le intestazioni refactored causano problemi di compilazione quando non richiedono più intestazioni particolari. Ad esempio, supponi di capire che RenderObject.hpp non ha effettivamente bisogno di Texture.hpp stesso.

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

    
risposta data 07.11.2014 - 05:44
fonte
21

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

Anche se siamo sull'argomento, dovresti includere solo altri file di intestazione nel file di intestazione se ne hai effettivamente bisogno nell'intestazione. Se ne hai solo bisogno nel file .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 realmente bisogno.

    
risposta data 07.11.2014 - 05:45
fonte
10

I keep wondering whether or not I should explicitly include all headers used directly in a particular file

Sì.

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

Disponiamo di guardie di intestazione per garantire che la doppia inclusione non sia dannosa.

    
risposta data 07.11.2014 - 12:28
fonte
3

Le opinioni divergono su questo, ma sono del parere che ogni file (che sia il file di origine c / cpp o il file di intestazione h / hpp) possa essere compilato o analizzato da solo.

Pertanto, tutti i file dovrebbero includere tutti i file di intestazione di cui hanno bisogno. Non dovresti assumere che un file di intestazione sia già stato incluso in precedenza.

È un vero dolore se devi aggiungere un file di intestazione e scoprire che usa un oggetto che è definito altrove, senza includerlo direttamente ... quindi devi andare a cercare (e probabilmente finire con quello sbagliato !)

Dall'altro lato, non importa (come regola generale) è importante se tu includi un file che non ti serve ...

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

    
risposta data 07.11.2014 - 10:02
fonte
2

Dipende se l'inclusione transitiva è per necessità (ad es. classe base) o a causa di un dettaglio di implementazione (membro privato).

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

Esempio: A.h è incluso da B.h che è usato da C.cpp. Se B.h ha usato A.h per alcuni dettagli di implementazione, quindi C.cpp non dovrebbe presumere che B.h continuerà a farlo. Ma se B.h usa A.h per una classe base, allora C.cpp può presumere che B.h continuerà a includere le intestazioni rilevanti per le sue classi base.

Vedi qui il vantaggio effettivo di NON duplicare inclusioni di intestazione. Dite che la classe base usata da B.h in realtà non apparteneva ad A.h e viene refactored in B.h stessa. B.h è ora un header standalone. Se C.cpp includeva in modo ridondante A.h, ora include un'intestazione non necessaria.

    
risposta data 07.11.2014 - 11:57
fonte
1

Sto prendendo un approccio leggermente diverso dalle risposte proposte.

Nelle intestazioni, includi sempre solo il minimo indispensabile, solo ciò che è necessario per fare passare la compilazione. Utilizza la dichiarazione anticipata ove possibile.

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

Per i piccoli progetti, inclusi gli header qui e là non farà la differenza. Ma per progetti di dimensioni medio-grandi, può diventare un problema. Anche se l'hardware più recente è utilizzato per la compilazione, la differenza può essere notevole. Il motivo è che il compilatore deve ancora aprire l'intestazione inclusa e analizzarlo. Quindi, per ottimizzare la build, applica la tecnica precedente (includi il minimo indispensabile e utilizza la dichiarazione in avanti).

Anche se un po 'obsoleto, Progettazione software su larga scala in C ++ (di John Lakos ) spiega tutto questo in dettaglio.

    
risposta data 07.11.2014 - 11:37
fonte
1

Ci può essere un altro caso: Hai A.h, B.h e il tuo C.cpp, B.h include A.h

quindi 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 "A.h" qui, cosa può accadere? nel tuo C.cpp, vengono utilizzati sia A che B (ad esempio classe). Più tardi hai cambiato il tuo codice C.cpp, rimuovi la roba relativa a B, ma lasciando B.h inclusa lì.

Se includi sia A.h che B.h e ora, a questo punto, gli strumenti che rilevano gli accessi non necessari potrebbero aiutarti a indicare che l'inclusione di B.h non è più necessaria. Se includi solo B.h come sopra, allora è difficile per gli strumenti / umani rilevare l'inclusione non necessaria dopo il cambio di codice.

    
risposta data 20.03.2018 - 06:55
fonte
-4

La buona pratica è non preoccuparti della tua strategia di intestazione finché compila.

La sezione di intestazione del tuo codice è solo un blocco di linee che nessuno dovrebbe nemmeno guardare fino a quando non ottieni un errore di compilazione facilmente risolto. Capisco il desiderio di uno stile 'corretto', ma in nessun modo si può davvero definirlo corretto. Includere un'intestazione per ogni classe è più probabile che causi fastidiosi errori di compilazione basati sugli ordini, ma gli errori di compilazione riflettono anche i problemi che potrebbero essere risolti con un'attenta codifica (anche se probabilmente non valgono la pena risolvere il problema).

E sì, avremo quei problemi basati sugli ordini una volta che inizi a salire su friend land.

Puoi pensare al problema in due casi.

Caso 1: hai un piccolo numero di classi che interagiscono tra loro, diciamo meno di una dozzina. aggiungi, rimuovi da, e in altro modo modifica queste intestazioni, in modi che possono influenzare le loro dipendenze l'una dall'altra. Questo è il caso suggerito dal tuo esempio di codice.

Il set di intestazioni è abbastanza piccolo da non complicare la risoluzione di eventuali problemi. Qualsiasi problema difficile viene risolto riscrivendo una o due intestazioni. Preoccuparti della tua strategia di intestazione significa risolvere problemi che non esistono.

Caso 2: Hai dozzine di classi. Alcune delle 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. Questo rappresenta una tipica impostazione aziendale. Le intestazioni sono distribuite tra le directory e non è possibile ricordare realisticamente i nomi di tutto.

Soluzione: a questo punto, devi pensare alle tue classi in gruppi logici e raggruppare quei gruppi in intestazioni che ti impediscono di dover ricorrere a #include ancora e ancora. Questo non solo semplifica la vita, ma è anche un passo necessario per sfruttare le intestazioni precompilate .

Finisci con #include di classi che non ti servono ma chi se ne frega ?

In questo caso, il tuo codice sarà simile a ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}
    
risposta data 07.11.2014 - 20:47
fonte

Leggi altre domande sui tag