È buona norma inserire le definizioni C ++ nei file di intestazione?


196

Il mio stile personale con C ++ deve sempre mettere le dichiarazioni di classe in un file include e le definizioni in un file .cpp, proprio come stabilito nella risposta di Loki ai file di intestazione C ++, Separazione del codice . Certo, parte del motivo per cui mi piace questo stile probabilmente ha a che fare con tutti gli anni trascorsi a programmare Modula-2 e Ada, entrambi i quali hanno uno schema simile con file di specifiche e file di body.

Ho un collega, molto più esperto in C ++ di me, che insiste sul fatto che tutte le dichiarazioni C ++ dovrebbero, ove possibile, includere le definizioni proprio lì nel file di intestazione. Non sta dicendo che questo è uno stile alternativo valido, o anche uno stile leggermente migliore, ma piuttosto questo è il nuovo stile universalmente accettato che tutti ora usano per C ++.

Non sono agile come una volta, quindi non sono davvero ansioso di arrampicarmi su questo suo carro finché non vedrò altre persone lassù con lui. Quindi quanto è comune questo idioma?

Giusto per dare una struttura alle risposte: è ora The Way , molto comune, un po 'comune, non comune o fuori di testa?


Le funzioni di una riga (getter e setter) nell'intestazione sono comuni. Più a lungo di quanto otterrebbe una seconda occhiata interrogativa. Forse per la definizione completa di una piccola classe che viene utilizzata solo da un'altra nella stessa intestazione?
Martin Beckett,

Finora ho sempre inserito tutte le definizioni delle mie classi nelle intestazioni. solo le definizioni per le classi pimpl sono le eccezioni. Dichiaro solo quelli nelle intestazioni.
Johannes Schaub - litb

3
Forse pensa che sia così perché è così che Visual C ++ insiste sul fatto che il codice sia scritto. Quando si fa clic su un pulsante, l'implementazione viene generata nel file di intestazione. Non so perché Microsoft dovrebbe incoraggiarlo, anche se per i motivi che altri hanno spiegato di seguito.
WKS,

4
@WKS - Microsoft preferirebbe che tutti programmassero in C #, e in C # non c'è distinzione "header" vs "body", è solo un file. Essendo in entrambi i mondi C ++ e C # da molto tempo ormai, il modo C # è in realtà molto più facile da gestire.
Mark Lakata,

1
@MarkLakata - Questa è davvero una delle cose che ha indicato. Non ho sentito da lui questa discussione di recente, ma IIRC ha sostenuto che Java e C # funzionano in questo modo, e C # era nuovo di zecca all'epoca, il che ha reso una tendenza che presto seguiranno tutte le lingue
TED

Risposte:


209

Il tuo collega ha torto, il modo comune è ed è sempre stato quello di inserire il codice nei file .cpp (o qualunque estensione ti piaccia) e le dichiarazioni nelle intestazioni.

Occasionalmente c'è qualche merito nel mettere il codice nell'intestazione, questo può consentire un allineamento più intelligente da parte del compilatore. Allo stesso tempo, può distruggere i tempi di compilazione poiché tutto il codice deve essere elaborato ogni volta che viene incluso dal compilatore.

Infine, è spesso fastidioso avere relazioni a oggetti circolari (a volte desiderate) quando tutto il codice è rappresentato dalle intestazioni.

In conclusione, avevi ragione, ha torto.

EDIT: Ho pensato alla tua domanda. C'è un caso in cui ciò che dice è vero. modelli. Molte nuove librerie "moderne" come boost fanno un uso pesante di modelli e spesso sono "solo intestazione". Tuttavia, questo dovrebbe essere fatto solo quando si tratta di modelli in quanto è l'unico modo per farlo quando si tratta di loro.

MODIFICARE: Alcune persone vorrebbero un po 'più di chiarimenti, ecco alcuni pensieri negativi a scrivere il codice "solo intestazione":

Se cerchi in giro, vedrai molte persone che cercano di trovare un modo per ridurre i tempi di compilazione quando si tratta di boost. Ad esempio: Come ridurre i tempi di compilazione con Boost Asio , che sta vedendo una compilazione 14s di un singolo file 1K con boost incluso. 14s potrebbe non sembrare "esploso", ma è sicuramente molto più lungo del normale e può sommarsi abbastanza rapidamente. Quando si tratta di un grande progetto. Le librerie solo intestazione influiscono sui tempi di compilazione in modo abbastanza misurabile. Lo tolleriamo solo perché boost è così utile.

Inoltre, ci sono molte cose che non possono essere fatte solo nelle intestazioni (anche boost ha librerie a cui devi collegarti per alcune parti come thread, filesystem, ecc.). Un esempio principale è che non è possibile avere oggetti globali semplici nelle librerie solo dell'intestazione (a meno che non si ricorra all'abominio che è un singleton) poiché si verificheranno errori di definizione multipla. NOTA: le variabili inline di C ++ 17 renderanno possibile questo esempio particolare in futuro.

Come ultimo punto, quando si utilizza boost come esempio di codice solo intestazione, spesso si perde un enorme dettaglio.

Boost è una libreria, non un codice a livello di utente. quindi non cambia così spesso. Nel codice utente, se metti tutto nelle intestazioni, ogni piccola modifica ti costringerà a ricompilare l'intero progetto. È una perdita di tempo monumentale (e non è il caso delle biblioteche che non cambiano da compilazione a compilazione). Quando dividi le cose tra intestazione / sorgente e ancora meglio, usa le dichiarazioni in avanti per ridurre le inclusioni, puoi risparmiare ore di ricompilazione una volta sommate in un giorno.


16
Sono abbastanza sicuro che è da dove lo sta ottenendo. Ogni volta che si presenta, fa apparire modelli. Il suo argomento è approssimativamente che dovresti fare tutto il codice in questo modo per coerenza.
TED

14
è un argomento scarso che sta sostenendo, atteniti alle tue armi :)
Evan Teran,

11
Le definizioni dei modelli possono essere nei file CPP se la parola chiave "export" è supportata. Questo è un angolo oscuro di C ++ che di solito non è nemmeno implementato dalla maggior parte delle compilazioni, per quanto ne so.
Andrei Taranchenko,

2
Vedi il fondo di questa risposta (la parte superiore è un po 'contorta) per un esempio: stackoverflow.com/questions/555330/…
Evan Teran,

3
Inizia a essere significativo per questa discussione in "Evviva, nessun errore del linker".
Evan Teran,

158

Il giorno in cui i programmatori C ++ concorderanno su The Way , gli agnelli si sdraieranno con i leoni, i palestinesi abbracceranno gli israeliani e cani e gatti potranno sposarsi.

La separazione tra i file .h e .cpp è per lo più arbitraria a questo punto, una traccia di ottimizzazioni del compilatore di lunga data. A mio avviso, le dichiarazioni appartengono all'intestazione e le definizioni appartengono al file di implementazione. Ma questa è solo un'abitudine, non una religione.


140
"Il giorno in cui i programmatori C ++ saranno d'accordo su The Way ..." rimarrà solo un programmatore C ++!
Brian Ensink,

9
Pensavo che fossero già d'accordo sulla strada, le dichiarazioni in .h e le definizioni in .cpp
hasen

6
Siamo tutti uomini ciechi e C ++ è un elefante.
Roderick Taylor,

abitudine? che ne dite di usare .h per definire l'ambito? da quale cosa è stato sostituito?
Hernán Eche,

28

Il codice nelle intestazioni è generalmente una cattiva idea poiché impone la ricompilazione di tutti i file che include l'intestazione quando si modifica il codice effettivo anziché le dichiarazioni. Rallenterà anche la compilazione poiché dovrai analizzare il codice in ogni file che include l'intestazione.

Un motivo per avere codice nei file di intestazione è che in genere è necessario che la parola chiave inline funzioni correttamente e quando si utilizzano modelli istanziati in altri file cpp.


1
"impone la ricompilazione di tutti i file che include l'intestazione quando si modifica il codice effettivo anziché le dichiarazioni" Penso che questa sia la ragione più genuina; si accompagna anche al fatto che le dichiarazioni nelle intestazioni cambiano meno frequentemente dell'implementazione nei file .c.
Ninad,

20

Ciò che potrebbe informare il tuo collega è l'idea che la maggior parte del codice C ++ dovrebbe essere modellata per consentire la massima usabilità. E se è basato su modelli, allora tutto dovrà essere in un file di intestazione, in modo che il codice client possa vederlo e crearne un'istanza. Se è abbastanza buono per Boost e STL, è abbastanza buono per noi.

Non sono d'accordo con questo punto di vista, ma potrebbe essere da dove viene.


Penso che tu abbia ragione su questo. Quando discutiamo che lui è sempre utilizzando l'esempio di modelli, in cui è più o meno devi fare questo. Non sono d'accordo con il "dover", ma le mie alternative sono piuttosto contorte.
TED,

1
@ted - per il codice basato sul modello è necessario inserire l'implementazione nell'intestazione. La parola chiave "export" consente a un compilatore di supportare la separazione della dichiarazione e la definizione di modelli, ma il supporto per l'esportazione è praticamente inesistente. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Michael Burr,

Un colpo di testa, sì, ma non deve essere la stessa intestazione. Vedi la risposta di sconosciuto sotto.
TED,

Questo ha senso, ma non posso dire di aver mai incontrato quello stile prima.
Michael Burr,

14

Penso che il tuo collega sia intelligente e anche tu abbia ragione.

Le cose utili che ho scoperto che mettere tutto nelle intestazioni è che:

  1. Non è necessario scrivere e sincronizzare intestazioni e fonti.

  2. La struttura è semplice e nessuna dipendenza circolare costringe il programmatore a creare una struttura "migliore".

  3. Portatile, facile da integrare in un nuovo progetto.

Sono d'accordo con il problema del tempo di compilazione, ma penso che dovremmo notare che:

  1. È molto probabile che il cambiamento del file sorgente cambi i file di intestazione che porta a ricompilare l'intero progetto.

  2. La velocità di compilazione è molto più veloce di prima. E se hai un progetto da costruire a lungo e ad alta frequenza, ciò potrebbe indicare che il design del tuo progetto ha dei difetti. Separare le attività in diversi progetti e moduli può evitare questo problema.

Infine, voglio solo supportare il tuo collega, solo dal mio punto di vista personale.


3
+1. Nessuno tranne te ha avuto l'idea che in un'intestazione proiettare solo tempi di compilazione lunghi possa suggerire troppe dipendenze, il che è un cattivo design. Buon punto! Ma queste dipendenze possono essere rimosse in una misura in cui il tempo di compilazione è effettivamente breve?
TobiMcNamobi,

@TobiMcNamobi: Adoro l'idea di "allentare" per ottenere un feedback migliore su decisioni di progettazione sbagliate. Tuttavia, nel caso del solo header vs compilato separatamente, se ci accontentiamo di quell'idea finiremo con un'unica unità di compilazione e tempi di compilazione enormi. Anche quando il design è davvero eccezionale.
Jo So,

In altre parole, la separazione tra interfaccia e implementazione è in realtà una parte del tuo progetto. In C, ti viene richiesto di esprimere le tue decisioni sull'incapsulamento attraverso la separazione nell'intestazione e l'implementazione.
Jo So,

1
Sto cominciando a chiedermi se ci siano degli svantaggi a far cadere le intestazioni del tutto come fanno le lingue moderne.
Jo So,

12

Metterò spesso banali funzioni membro nel file header, per consentire loro di essere incorporate. Ma per mettere lì tutto il codice, solo per essere coerenti con i template? È semplice.

Ricorda: una consistenza insensata è il folletto delle piccole menti .


Sì, lo faccio anche io. La regola generale che uso sembra essere qualcosa del tipo "se si adatta a una riga di codice, lasciarlo nell'intestazione".
TED,

Cosa succede quando una libreria fornisce il corpo di una classe modello A<B>in un file cpp e quindi l'utente desidera un A<C>?
jww

@jww Non l'ho dichiarato esplicitamente, ma le classi dei template dovrebbero essere completamente definite nelle intestazioni in modo che il compilatore possa istanziarlo con qualsiasi tipo di cui abbia bisogno. Questo è un requisito tecnico, non una scelta stilistica. Penso che il problema nella domanda originale sia che qualcuno ha deciso se fosse buono per i template, ma anche per le lezioni regolari.
Mark Ransom,

7

Come diceva Tuomas, l'intestazione dovrebbe essere minima. Per essere completo espanderò un po '.

Personalmente uso 4 tipi di file nei miei C++progetti:

  • Pubblico:
  • Intestazione di inoltro: in caso di modelli ecc., Questo file ottiene le dichiarazioni di inoltro che verranno visualizzate nell'intestazione.
  • Intestazione: questo file include l'eventuale intestazione di inoltro e dichiara tutto ciò che desidero essere pubblico (e definisce le classi ...)
  • Privato:
  • Intestazione privata: questo file è un'intestazione riservata per l'implementazione, include l'intestazione e dichiara le funzioni / strutture dell'helper (ad esempio Pimpl o predicati). Salta se non necessario.
  • File di origine: include l'intestazione privata (o l'intestazione se nessuna intestazione privata) e definisce tutto (non modello ...)

Inoltre, abbino questo ad un'altra regola: non definire ciò che puoi dichiarare in avanti. Anche se ovviamente sono ragionevole lì (usare Pimpl ovunque è piuttosto una seccatura).

Significa che preferisco una dichiarazione in avanti rispetto a un #include direttiva nelle mie intestazioni ogni volta che riesco a cavarmela.

Infine, utilizzo anche una regola di visibilità: limito il più possibile gli ambiti dei miei simboli in modo che non inquinino gli ambiti esterni.

Mettendo tutto insieme:

// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;

// example.hpp
#include "project/example_fwd.hpp"

// Those can't really be skipped
#include <string>
#include <vector>

#include "project/pimpl.hpp"

// Those can be forward declared easily
#include "project/foo_fwd.hpp"

namespace project { class Bar; }

namespace project
{
  class MyClass
  {
  public:
    struct Color // Limiting scope of enum
    {
      enum type { Red, Orange, Green };
    };
    typedef Color::type Color_t;

  public:
    MyClass(); // because of pimpl, I need to define the constructor

  private:
    struct Impl;
    pimpl<Impl> mImpl; // I won't describe pimpl here :p
  };

  template <class T> class MyClassT: public MyClass {};
} // namespace project

// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"

template <class T> void check(MyClass<T> const& c) { }

// example.cpp
#include "example_impl.hpp"

// MyClass definition

Il salvavita qui è che la maggior parte delle volte l'intestazione in avanti è inutile: necessaria solo in caso di typedefo templatecosì è l'intestazione di implementazione;)


6

Per aggiungere più divertimento è possibile aggiungere .ippfile che contengono l'implementazione del modello (che è incluso in .hpp), mentre .hppcontiene l'interfaccia.

A parte il codice templatizzato (a seconda del progetto questo può essere la maggioranza o la minoranza dei file) c'è un codice normale e qui è meglio separare le dichiarazioni e le definizioni. Fornire anche dichiarazioni a termine laddove necessario, ciò potrebbe influire sui tempi di compilazione.


Questo è quello che ho iniziato a fare anche con le definizioni dei modelli (anche se non sono sicuro di aver usato la stessa estensione ... è passato un po 'di tempo).
TED,

5

Generalmente, quando scrivo una nuova classe, inserirò tutto il codice nella classe, quindi non devo cercarlo in un altro file. Dopo che tutto funziona, rompo il corpo dei metodi nel file cpp , lasciando i prototipi nel file hpp.


4

Se questo nuovo modo è davvero The Way , avremmo potuto correre in direzioni diverse nei nostri progetti.

Perché cerchiamo di evitare tutte le cose inutili nelle intestazioni. Ciò include evitare la cascata dell'intestazione. Il codice nelle intestazioni avrà probabilmente bisogno di qualche altra intestazione da includere, che avrà bisogno di un'altra intestazione e così via. Se siamo costretti a utilizzare i modelli, proviamo a evitare di sporcare troppo le intestazioni con elementi del modello.

Inoltre usiamo il modello "puntatore opaco" quando applicabile, .

Con queste pratiche possiamo fare build più veloci della maggior parte dei nostri colleghi. E sì ... la modifica del codice o dei membri della classe non causerà enormi ricostruzioni.


4

Personalmente lo faccio nei miei file di intestazione:

// class-declaration

// inline-method-declarations

Non mi piace mescolare il codice per i metodi con la classe in quanto trovo difficile cercare rapidamente le cose.

Non aggiungerei TUTTI i metodi nel file di intestazione. Il compilatore (normalmente) non sarà in grado di incorporare metodi virtuali e (probabilmente) solo inline metodi piccoli senza loop (dipende totalmente dal compilatore).

Fare i metodi nella classe è valido ... ma da un punto di vista della leggibilità non mi piace. Mettere i metodi nell'intestazione significa che, quando possibile, verranno allineati.


2

IMHO, ha merito SOLO se sta facendo modelli e / o metaprogrammi. Ci sono molte ragioni già menzionate per limitare i file di intestazione a sole dichiarazioni. Sono solo ... intestazioni. Se si desidera includere il codice, compilarlo come libreria e collegarlo.


2

Ho messo tutta l'implementazione fuori dalla definizione di classe. Voglio avere i commenti doxygen fuori dalla definizione di classe.


1
So che è tardi, ma i downvoter (o simpatizzanti) si preoccupano di commentare perché? Questa mi sembra un'affermazione ragionevole. Usiamo Doxygen e il problema è sicuramente emerso.
TED

2

Penso che sia assolutamente assurdo inserire TUTTE le definizioni delle funzioni nel file di intestazione. Perché? Perché il file di intestazione viene utilizzato come interfaccia PUBLIC per la tua classe. È l'esterno della "scatola nera".

Quando devi guardare una classe per fare riferimento a come usarla, dovresti guardare il file header. Il file di intestazione dovrebbe fornire un elenco di ciò che può fare (commentato per descrivere i dettagli su come utilizzare ciascuna funzione) e dovrebbe includere un elenco delle variabili membro. NON DOVREBBE includere COME COME viene implementata ogni singola funzione, perché è un carico di informazioni non necessarie e ingombra solo il file di intestazione.


1

Non dipende davvero dalla complessità del sistema e dalle convenzioni interne?

Al momento sto lavorando a un simulatore di rete neurale che è incredibilmente complesso e lo stile accettato che dovrei usare è:

Definizioni di classe in classname.h
Codice di classe in classnameCode.h
codice eseguibile in classname.cpp

Ciò divide le simulazioni create dall'utente dalle classi di base create dallo sviluppatore e funziona meglio nella situazione.

Tuttavia, sarei sorpreso di vedere le persone fare questo, ad esempio, in un'applicazione grafica o in qualsiasi altra applicazione che abbia lo scopo di non fornire agli utenti una base di codice.


1
Qual è esattamente la distinzione tra "codice di classe" e "codice eseguibile"?
TED,

Come ho detto, è un simulatore neurale: l'utente crea simulazioni eseguibili che sono costruite su un gran numero di classi che fungono da neuroni ecc. Quindi il nostro codice è semplicemente classi che non possono effettivamente fare nulla da sole, e l'utente crea il codice eseguibile che fa fare cose al simulatore.
Ed James,

In generale, non potresti dire "non si può effettivamente fare nulla da solo" per la stragrande maggioranza (se non la totalità) di quasi tutti i programmi? Stai dicendo che il codice "principale" va in un cpp, ma nient'altro lo fa?
TED,

In questa situazione è un po 'diverso. Il codice che scriviamo è fondamentalmente una libreria e l'utente crea le proprie simulazioni, che sono effettivamente eseguibili. Pensaci come openGL -> ottieni un sacco di funzioni e oggetti ma senza un file cpp che può eseguirli sono inutili.
Ed James,

0

Il codice modello deve essere solo nelle intestazioni. A parte questo, tutte le definizioni tranne quelle in linea dovrebbero essere in .cpp. L'argomento migliore per questo sarebbe l'implementazione della libreria std che segue la stessa regola. Non saresti in disaccordo sul fatto che gli sviluppatori di std lib avrebbero ragione su questo.


Quali stdlibs? GCC's libstdc++sembra (AFAICS) che non inserisce quasi nulla srce quasi tutto include, indipendentemente dal fatto che "debba" essere presente in un'intestazione. Quindi non penso che questa sia una citazione accurata / utile. Ad ogni modo, non credo che gli stdlibs siano in gran parte un modello per il codice utente: sono ovviamente scritti da programmatori altamente qualificati, ma da usare , non leggere: astraggono l'elevata complessità che la maggior parte dei programmatori non dovrebbe pensare , hanno bisogno di brutte _Reserved __namesovunque per evitare conflitti con l'utente, i commenti e la spaziatura sono al di sotto di ciò che consiglierei, ecc. Sono esemplari in modo ristretto.
underscore_d

0

Penso che il tuo collega abbia ragione purché non entri nel processo per scrivere il codice eseguibile nell'intestazione. Il giusto equilibrio, penso, è seguire il percorso indicato da GNAT Ada in cui il file .ads fornisce una definizione dell'interfaccia perfettamente adeguata del pacchetto per i suoi utenti e per i suoi figli.

A proposito Ted, hai dato un'occhiata a questo forum alla recente domanda sull'associazione di Ada alla libreria CLIPS che hai scritto diversi anni fa e che non è più disponibile (le pagine Web pertinenti sono ora chiuse). Anche se realizzato con una vecchia versione di Clips, questa associazione potrebbe essere un buon esempio di partenza per qualcuno disposto a utilizzare il motore di inferenza CLIPS all'interno di un programma Ada 2012.


1
Lol. 2 anni dopo, questo è un modo strano di entrare in contatto con qualcuno. Controllerò se ne ho ancora una copia, ma molto probabilmente no. L'ho fatto per una classe AI in modo da poter fare il mio codice in Ada, ma ho intenzionalmente fatto quel progetto CC0 (essenzialmente non protetto da copyright) nella speranza che qualcuno lo prendesse spudoratamente e facesse qualcosa con esso.
TED
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.