Pro e contro di inserire tutto il codice nei file di intestazione in C ++?

È ansible strutturare un programma C ++ in modo che (quasi) tutto il codice risieda nei file di intestazione. Sembra essenzialmente un programma C # o Java. Tuttavia, è necessario almeno un file .cpp per inserire tutti i file di intestazione durante la compilazione. Ora so che alcune persone detesterebbero assolutamente questa idea. Ma non ho trovato nessun convincente svantaggio di fare questo. Posso elencare alcuni vantaggi:

[1] Tempi di compilazione più rapidi. Tutti i file di intestazione vengono analizzati solo una volta, poiché esiste un solo file .cpp. Inoltre, un file di intestazione non può essere incluso più di una volta, altrimenti si otterrà un’interruzione di build. Ci sono altri modi per ottenere compilazioni più veloci quando si utilizza l’approccio alternativo, ma questo è così semplice.

[2] Evita le dipendenze circolari, rendendole assolutamente chiare. Se ClassA in ClassA.h ha una dipendenza circolare su ClassB in ClassB.h , devo mettere un riferimento in avanti e sporge. (Si noti che questo è diverso da C # e Java, in cui il compilatore risolve automaticamente le dipendenze circolari, il che incoraggia le pratiche di codifica errate IMO). Di nuovo, puoi evitare le dipendenze circolari se il tuo codice era in file .cpp , ma in un progetto reale i file .cpp tendono ad includere intestazioni casuali fino a quando non riesci a capire chi dipende da chi.

I tuoi pensieri?

Ragione [1] Tempi di compilazione più rapidi

Non nei miei progetti: i file di origine (CPP) includono solo le intestazioni (HPP) di cui hanno bisogno. Quindi, quando ho bisogno di ricompilare solo un CPP a causa di un piccolo cambiamento, ho dieci volte lo stesso numero di file che non vengono ricompilati.

Forse dovresti abbattere il tuo progetto in sorgenti / intestazioni più logiche: una modifica nell’implementazione della class A NON dovrebbe richiedere la ricompilazione delle implementazioni di class B, C, D, E, ecc.

Motivo [2] Evita le dipendenze circolari

Dipendenze circolari nel codice?

Scusa, ma devo ancora avere questo tipo di problema come un vero problema: Diciamo che A dipende da B, e B dipende da A:

 struct A { B * b ; void doSomethingWithB() ; } ; struct B { A * a ; void doSomethingWithA() ; } ; void A::doSomethingWithB() { /* etc. */ } void B::doSomethingWithA() { /* etc. */ } 

Un buon modo per risolvere il problema sarebbe quello di suddividere questa fonte in almeno una fonte / intestazione per class (in un modo simile a quello di Java, ma con un’origine e un’intestazione per class):

 // A.hpp struct B ; struct A { B * b ; void doSomethingWithB() ; } ; 

.

 // B.hpp struct A ; struct B { A * a ; void doSomethingWithA() ; } ; 

.

 // A.cpp #include "A.hpp" #include "B.hpp" void A::doSomethingWithB() { /* etc. */ } 

.

 // B.cpp #include "B.hpp" #include "A.hpp" void B::doSomethingWithA() { /* etc. */ } 

Quindi, nessun problema di dipendenza e tempi di compilazione ancora veloci.

Ho dimenticato qualcosa?

Quando si lavora su progetti “reali”

in un progetto reale, i file cpp tendono ad includere intestazioni casuali fino a quando non riesci a capire chi dipende da chi

Ovviamente. Ma se hai tempo di riorganizzare questi file per build la tua soluzione “un CPP”, allora hai tempo per pulire quelle intestazioni. Le mie regole per le intestazioni sono:

  • abbattere l’intestazione per renderli il più modulari ansible
  • Non includere mai intestazioni che non ti servono
  • Se hai bisogno di un simbolo, inoltralo – dichiaralo
  • solo se il precedente non è riuscito, includi l’intestazione

Ad ogni modo, tutte le intestazioni devono essere autosufficienti, il che significa:

  • Un’intestazione include tutte le intestazioni necessarie (e solo le intestazioni necessarie – vedi sopra)
  • un file CPP vuoto che include un’intestazione deve essere compilato senza bisogno di includere altro

Ciò eliminerà i problemi di ordinamento e le dipendenze circolari.

La compilazione è un problema? Poi…

Se il tempo di compilazione fosse davvero un problema, prenderei in considerazione entrambi:

  • Usando le intestazioni precompilate (questo è abbastanza utile per STL e BOOST)
  • Diminuire l’accoppiamento tramite l’idioma PImpl, come spiegato in http://en.wikipedia.org/wiki/Opaque_pointer
  • Usa la compilazione condivisa di rete

Conclusione

Quello che stai facendo non sta mettendo tutto nelle intestazioni.

In pratica stai includendo tutti i tuoi file in un’unica e unica fonte finale.

Forse stai vincendo in termini di compilazione di progetti completi.

Ma quando compili per un piccolo cambiamento, perderai sempre.

Durante la codifica, so di dover compilare spesso piccole modifiche (se non altro per fare in modo che il compilatore convalidi il mio codice), e quindi un’ultima volta, eseguire un cambiamento completo del progetto.

Perderei molto tempo se il mio progetto fosse organizzato a modo tuo.

Non sono d’accordo con il punto 1.

Sì, c’è solo uno .cpp e il tempo costruito da zero è più veloce. Ma raramente si costruisce da zero. Apportate piccole modifiche e ogni volta sarà necessario ricompilare l’intero progetto.

Preferisco farlo al contrario:

  • mantenere le dichiarazioni condivise in file .h.
  • mantieni la definizione per le classi che vengono utilizzate solo in un posto nei file .cpp

Quindi, alcuni dei miei file .cpp iniziano ad apparire come codice Java o C #;)

Ma l’approccio “mantenere la roba in .h” è buono durante la progettazione del sistema, a causa del punto 2. che hai creato. Di solito lo faccio mentre sto costruendo la gerarchia delle classi e più tardi quando l’architettura del codice diventa stabile, sposto il codice in file .cpp.

Hai ragione a dire che la tua soluzione funziona. Potrebbe anche non avere svantaggi per il tuo attuale progetto e ambiente di sviluppo.

Ma…

Come altri hanno affermato, inserire tutto il codice nei file di intestazione forza una compilazione completa ogni volta che si modifica una riga di codice. Questo potrebbe non essere ancora un problema, ma il tuo progetto potrebbe crescere abbastanza al punto che il tempo di compilazione sarà un problema.

Un altro problema è quando si condivide il codice. Anche se potresti non essere direttamente interessato, è importante tenere nascosto il maggior numero ansible di codice da un potenziale utente del tuo codice. Inserendo il tuo codice nel file di intestazione, qualsiasi programmatore che utilizza il tuo codice deve guardare l’intero codice, mentre ci sono solo interesse su come usarlo. Mettendo il tuo codice nel file cpp puoi solo consegnare un componente binario (una libreria statica o dynamic) e la sua interfaccia come file di intestazione, che potrebbe essere più semplice in qualche ambiente.

Questo è un problema se vuoi essere in grado di trasformare il tuo codice corrente in una libreria dynamic. Poiché non si dispone di una dichiarazione di interfaccia corretta disaccoppiata dal codice effettivo, non sarà ansible fornire una libreria dynamic compilata e la sua interfaccia di utilizzo come file di intestazione leggibili.

Potresti non avere ancora questi problemi, ecco perché stavo dicendo che la tua soluzione potrebbe essere ok nel tuo ambiente attuale. Ma è sempre meglio essere preparati a qualsiasi cambiamento e alcuni di questi problemi dovrebbero essere affrontati.

PS: A proposito di C # o Java, dovresti tenere a mente che queste lingue non stanno facendo quello che dici. In realtà stanno compilando i file in modo indipendente (come i file cpp) e memorizzano l’interfaccia globalmente per ogni file. Queste interfacce (e qualsiasi altra interfaccia collegata) vengono quindi utilizzate per colbind l’intero progetto, per questo sono in grado di gestire riferimenti circolari. Poiché C ++ esegue solo un passaggio di compilazione per file, non è in grado di archiviare globalmente le interfacce. Ecco perché ti viene richiesto di scriverli esplicitamente nei file di intestazione.

Hai frainteso il modo in cui il linguaggio era destinato ad essere utilizzato. I file .cpp sono veramente (o dovrebbero essere ad eccezione del codice inline e template) gli unici moduli del codice eseguibile che hai nel tuo sistema. I file .cpp sono compilati in file object che vengono poi collegati insieme. I file .h esistono esclusivamente per la dichiarazione anticipata del codice implementato nei file .cpp.

Ciò si traduce in un tempo di compilazione più veloce e un eseguibile più piccolo. Sembra anche molto più pulito perché puoi ottenere una rapida panoramica della tua class osservando la sua dichiarazione .h.

Per quanto riguarda il codice inline e template – poiché entrambi sono usati per generare codice dal compilatore e non dal linker – devono sempre essere disponibili per il compilatore per file .cpp. Quindi l’unica soluzione è includerla nel tuo file .h.

Tuttavia, ho sviluppato una soluzione in cui ho la mia dichiarazione di class in un file .h, tutto il codice template e inline in un file .inl e tutte le implementazioni di codice non in template / inline nel mio file .cpp. Il file .inl è #incluso nella parte inferiore del mio file .h. Ciò mantiene le cose pulite e coerenti.

L’ovvio lato negativo per me è che devi sempre creare tutto il codice in una sola volta. Con i file .cpp , puoi avere una compilazione separata, quindi stai ricostruendo solo i bit che sono davvero cambiati.

Potresti voler controllare Lazy C ++ . Permette di posizionare tutto in un singolo file e poi viene eseguito prima della compilazione e suddivide il codice in file .h e .cpp. Questo potrebbe offrirti il ​​meglio di entrambi i mondi.

I tempi di compilazione lenti sono solitamente dovuti a un eccessivo accoppiamento all’interno di un sistema scritto in C ++. Forse hai bisogno di dividere il codice in sottosistemi con interfacce esterne. Questi moduli potrebbero essere compilati in progetti separati. In questo modo è ansible ridurre al minimo la dipendenza tra diversi moduli del sistema.

Una cosa che stai rinunciando a cui farei fatica a vivere senza spazi dei nomi anonimi.

Trovo che siano incredibilmente utili per definire funzioni di utilità specifiche della class che dovrebbero essere invisibili al di fuori del file di implementazione della class. Sono anche ideali per contenere dati globali che dovrebbero essere invisibili al resto del sistema, come un’istanza singleton.

Stai andando al di fuori dell’ambito di progettazione della lingua. Mentre potresti avere alcuni benefici, alla fine ti morderà il sedere.

C ++ è progettato per file h con dichiarazioni e file cpp con implementazioni. I compilatori sono costruiti attorno a questo progetto.

, la gente discute se sia una buona architettura, ma è il design. È meglio dedicare il proprio tempo al problema piuttosto che reinventare nuovi modi per progettare l’architettura di file C ++.

Uno svantaggio del tuo approccio è che non puoi eseguire la compilazione parallela. Potresti pensare di ottenere una compilazione più veloce ora, ma se disponi di più file .cpp puoi crearli in parallelo su più core sulla tua macchina o utilizzando un sistema di build distribuito come distcc o Incredibuild.

Mi piace pensare alla separazione dei file .h e .cpp in termini di interfacce e implementazioni. I file .h contengono le descrizioni dell’interfaccia per un’altra class e i file .cpp contengono le implementazioni. A volte ci sono problemi pratici o chiarezza che impediscono una separazione completamente pulita, ma è da lì che inizio. Ad esempio, piccole funzioni di accesso di solito in codice nella dichiarazione di class per chiarezza. Le funzioni più grandi sono codificate nel file .cpp

In ogni caso, non lasciare che i tempi di compilazione dettino come strutturerai il tuo programma. Meglio avere un programma leggibile e gestibile su uno che compili in un minuto e mezzo anziché in 2 minuti.

Credo che, a meno che non si utilizzino le intestazioni precompilate di MSVC e si utilizzi un Makefile o un altro sistema di creazione basato su dipendenze, la compilazione iterativa di file di origine separati dovrebbe essere più rapida. Dato che il mio sviluppo è quasi sempre iterativo, mi interessa più di quanto velocemente possa ricompilare le modifiche apportate nel file x.cpp che negli altri venti file sorgente che non ho modificato. Inoltre, apporto le modifiche molto più frequentemente ai file di origine di quanto non faccia alle API in modo che cambino meno frequentemente.

Riguardo alle dipendenze circolari. Avrei preso il consiglio di paercebal un passo più in là. Aveva due classi che avevano punti di riferimento l’un l’altro. Invece, mi imbatto più spesso nel caso in cui una class richiede un’altra class. Quando ciò accade, includo il file di intestazione per la dipendenza nel file di intestazione dell’altra class. Un esempio:

 // foo.hpp #ifndef __FOO_HPP__ #define __FOO_HPP__ struct foo { int data ; } ; #endif // __FOO_HPP__ 

.

 // bar.hpp #ifndef __BAR_HPP__ #define __BAR_HPP__ #include "foo.hpp" struct bar { foo f ; void doSomethingWithFoo() ; } ; #endif // __BAR_HPP__ 

.

 // bar.cpp #include "bar.hpp" void bar::doSomethingWithFoo() { // Initialize f f.data = 0; // etc. } 

Il motivo per cui includo questo, che è leggermente estraneo alle dipendenze circolari, è che ritengo che ci siano alternative per includere i file di intestazione, volenti o nolenti. In questo esempio il file sorgente della struct bar non include il file di intestazione struct foo. Questo è fatto nel file di intestazione. Questo ha un vantaggio nel fatto che uno sviluppatore che usa la barra non deve sapere di altri file che lo sviluppatore dovrebbe includere per usare quel file di intestazione.

Un problema con il codice nelle intestazioni è che deve essere in linea, altrimenti si avranno problemi di definizione multipla quando si collegano più unità di traduzione che includono la stessa intestazione.

La domanda originale specificava che nel progetto esisteva sempre un solo cpp, ma non è così se si crea un componente destinato a una libreria riutilizzabile.

Pertanto, nell’interesse di creare il codice più riutilizzabile e manutenibile ansible, inserire solo il codice inline e inlinabile nei file di intestazione.

Bene, come molti hanno sottolineato, ci sono molti svantaggi a questa idea, ma per bilanciare un po ‘e fornire un professionista, direi che avere un codice di libreria interamente nelle intestazioni ha senso, dal momento che lo renderà indipendente dagli altri impostazioni nel progetto in cui è utilizzato.

Ad esempio, se si sta tentando di utilizzare diverse librerie Open Source, è ansible impostare l’utilizzo di approcci diversi per il collegamento al proprio programma: alcuni possono utilizzare il codice libreria caricato dynamicmente del sistema operativo, altri sono impostati per essere collegati staticamente; alcuni possono essere impostati per utilizzare il multithreading, mentre altri no. E potrebbe benissimo essere un compito travolgente per un programmatore – specialmente con un limite di tempo – cercare di ordinare questi approcci incompatibili.

Tutto ciò non è tuttavia un problema quando si utilizzano librerie contenute interamente nelle intestazioni. “Funziona solo” per una ragionevole libreria ben scritta.

i kludges statici o globali-variabili ancora meno trasparenti, forse non debuggibili.

per esempio contando il numero totale di iterazioni per l’analisi.

Nei miei file kludged mettere tali elementi nella parte superiore del file cpp li rende facili da trovare.

Con “forse non debuggable”, intendo che di routine inserirò un tale globale nella finestra WATCH. Dal momento che è sempre nel campo di applicazione, la finestra WATCH può sempre arrivare ad esso indipendentemente da dove si trovi il contatore del programma in questo momento. Inserendo tali variabili al di fuori di un {} all’inizio di un file di intestazione si lascerebbe che tutto il codice downstream li “vedesse”. Mettendoli INSIDE a {}, in modo offensivo, penserei che il debugger non li considererà più “in-scope” se il contatore del programma è esterno a {}. Considerando che con kludge-global-at-Cpp-top, anche se potrebbe essere globale fino al punto di mostrare nel tuo link-map-pdb-etc, senza una dichiarazione extern gli altri file Cpp non riescono ad arrivare ad esso , evitando l’accoppiamento accidentale.

Una cosa che nessuno ha sollevato è che la compilazione di file di grandi dimensioni richiede molta memoria. Compilare il tuo intero progetto in una volta richiede uno spazio di memoria così grande che non è fattibile anche se potresti inserire tutto il codice nelle intestazioni.

Se stai usando classi template, devi comunque inserire l’intera implementazione nell’intestazione …

Compilare l’intero progetto in una volta sola (attraverso un singolo file .cpp di base) dovrebbe consentire qualcosa come “Ottimizzazione del programma intero” o “Ottimizzazione dei moduli incrociati”, che è disponibile solo in alcuni compilatori avanzati. Questo non è realmente ansible con un compilatore standard se stai precompilando tutti i tuoi file .cpp in file object, quindi collegandoli.

L’importante filosofia della programmazione orientata agli oggetti consiste nell’avere nascosto i dati che conducono a classi incapsulate con implementazione nascosta agli utenti. Ciò serve principalmente a fornire un livello di astrazione in cui gli utenti di una class utilizzano principalmente le funzioni dei membri accessibili pubblicamente, ad esempio per i tipi specifici e quelli statici. Quindi lo sviluppatore della class è libero di modificare le implementazioni effettive purché le implementazioni non siano esposte agli utenti. Anche se l’implementazione è privata e dichiarata in un file di intestazione, la modifica dell’implementazione richiederà la ricompilazione di tutti i codebase dipendenti. Mentre, se l’implementazione (definizione delle funzioni membro) si trova in un codice sorgente (file non di intestazione), la libreria viene modificata e la base di codice dipendente deve ricollegarsi alla versione rivista della libreria. Se quella libreria è collegata dynamicmente come una libreria condivisa, mantenendo la firma della funzione (interfaccia) stessa e l’implementazione modificata non richiede anche il ricollegamento. Vantaggio? Ovviamente.