PDA

Visualizza la versione completa : [OT] Perché C++ divide il codice in file .h e .cpp?


Kinn
23-04-2018, 15:17
Salve,
Sebbene la domanda possa lasciarlo intendere, non sono un programmatore alle prime armi. A causa dei miei studi ho grande esperienza con Java ma poca con C++. Per motivi lavorativi oggi sto riprendendo ciò che ho studiato all'università su quest'ultimo.

Ad oggi non ricordo, e sul web non trovo risposte soddisfacenti, sul perché in C++ è uso comune dividere le definizioni di classi in header e le implementazioni dei body in cpp.
Il mio insegnante per altro utilizzava entrambi i metodi, talvolta utilizzando solo file cpp, cosa che ho sempre ritenuto più naturale (probabilmente a causa di java).

Mi sono sempre chiesto se sia una "violazione" includere tutto il codice in file cpp o se ci sono motivi validi per dividere il codice usando headers, soprattutto da un punto di vista di qualità del codice.

torn24
24-04-2018, 10:32
Non sono un esperto, ma non so se un esperto risponderebbe alla tua domanda e quindi provo a darti una risposta :)
In C/C++ per poter utilizzare una classe, questa deve essere dichiarata nel sorgente che ne fa uso. Il C/C++ ammette più dichiarazioni ma una sola implementazione della classe.
Una stessa classe può essere usata in più sorgenti, se io separo la dichiarazione in un file header, lo posso includere in tutti i sorgenti e avrò tante dichiarazioni, ma comunque una sola implementazione nel file .cpp.
Se io definisco i metodi all'interno della classe, e dovessi usare la classe in più sorgenti,dovrei includere il file nei sorgenti che ne fanno uso, ma il linker mi darebbe errore perché esisterebbero più implementazioni della classe.
Quindi se la classe la uso in un unico sorgente potrei implementarla in un unico file "sconsigliato ma non vietato", se la uso in più sorgenti sono obbligato a separare la dichiarazione dalla implementazione.

Kinn
24-04-2018, 11:49
Non sono un esperto, ma non so se un esperto risponderebbe alla tua domanda e quindi provo a darti una risposta :)
In C/C++ per poter utilizzare una classe, questa deve essere dichiarata nel sorgente che ne fa uso. Il C/C++ ammette più dichiarazioni ma una sola implementazione della classe.
Una stessa classe può essere usata in più sorgenti, se io separo la dichiarazione in un file header, lo posso includere in tutti i sorgenti e avrò tante dichiarazioni, ma comunque una sola implementazione nel file .cpp.
Se io definisco i metodi all'interno della classe, e dovessi usare la classe in più sorgenti,dovrei includere il file nei sorgenti che ne fanno uso, ma il linker mi darebbe errore perché esisterebbero più implementazioni della classe.
Quindi se la classe la uso in un unico sorgente potrei implementarla in un unico file "sconsigliato ma non vietato", se la uso in più sorgenti sono obbligato a separare la dichiarazione dalla implementazione.

Punto valido il tuo, sicuramente dev'essere almeno uno dei motivi.

Mi piacerebbe sapere se in ambito professionale unire implementazione e dichiarazione sia considerata una pratica abietta. Del resto non mi è mai capitato di dover riutilizzare la dichiarazione di una classe per un'altra implementazione, al più mi sembra più adeguato il concetto di polimorfismo ed ereditarietà.

shodan
24-04-2018, 13:36
I motivi sono principalmente storici in realtà. Dato che il C++ discende dal C ne ha ereditato anche il modello file.h, file.cpp e tutti i motivi addotti sono figli di questo modello.
Nella community C++ sono tanti ad essere infastiditi da questo modello arcaico e attualmente il comitato ISOC++ sta lavorando sui modules che dovrebbero semplificare la situazione (qualcosa simil Java), ma prima che siano pienamente supportati ci vorrà qualche anno.


Mi piacerebbe sapere se in ambito professionale unire implementazione e dichiarazione sia considerata una pratica abietta.

Dipende. Se stai lavorando su una classe template sei obbligato ad avere dichiarazioni e definizioni in un unico file .h.
Se la classe non è un template e ha pochi ( < 4 o 5) metodi con poco codice dentro, può essere conveniente mettere tutto nel corpo della classe o appena sotto la dichiarazione ponendo i metodi inline.
Se la classe è grossa, c'è il rischio che il codice finale sia molto più grosso di quello che si pensi (il bloatware insomma).
Per cui in C++ non è abbietto, ma poco pratico.
Per farti l'esempio opposto, quando ho studiato Java mi sono sempre chiesto perché sono obbligato a definire i metodi dentro le classi, impedendo di avere una dichiarazione di classe pulita e i metodi implementati in maniera altrettanto pulita appena dopo la dichiarazione di classe.

Kinn
24-04-2018, 13:43
I motivi sono principalmente storici in realtà. Dato che il C++ discende dal C ne ha ereditato anche il modello file.h, file.cpp e tutti i motivi addotti sono figli di questo modello.
Nella community C++ sono tanti ad essere infastiditi da questo modello arcaico e attualmente il comitato ISOC++ sta lavorando sui modules che dovrebbero semplificare la situazione (qualcosa simil Java), ma prima che siano pienamente supportati ci vorrà qualche anno.

Dipende. Se stai lavorando su una classe template sei obbligato ad avere dichiarazioni e definizioni in un unico file .h.
Se la classe non è un template e ha pochi ( < 4 o 5) metodi con poco codice dentro, può essere conveniente mettere tutto nel corpo della classe o appena sotto la dichiarazione ponendo i metodi inline.
Se la classe è grossa, c'è il rischio che il codice finale sia molto più grosso di quello che si pensi (il bloatware insomma).
Per cui in C++ non è abbietto, ma poco pratico.
Per farti l'esempio opposto, quando ho studiato Java mi sono sempre chiesto perché sono obbligato a definire i metodi dentro le classi, impedendo di avere una dichiarazione di classe pulita e i metodi implementati in maniera altrettanto pulita appena dopo la dichiarazione di classe.

Grazie mille della risposta

MItaly
25-04-2018, 16:49
Il problema di fondo che si cerca di risolvere con la separazione .h/.cpp alla fine è quello dei tempi di compilazione.

In un header di norma si cerca di mettere tutto e solo quello che è necessario ad un'altra translation unit (=file .cpp) per poter usare le entità in esso dichiarate. Per cui, nel .h ci sarà la dichiarazione dell'interfaccia della classe e dei membri in essa contenuti (anche privati - necessario per via della semantica by-value del C++, per cui chi istanzia una classe sullo stack o come membro di un'altra classe ha necessariamente bisogno di sapere quanto è grande; questa limitazione ha dato origine ad idiomi come il cosiddetto PIMPL per poterla aggirare). L'implementazione concreta dei metodi (o delle funzioni o di dettagli implementativi) invece sta in genere dentro al corrispondente .cpp.

Questo fa sì che durante lo sviluppo non sia necessario ri-parsare e ri-compilare tutti i corpi di metodi e funzioni - se il .cpp non è stato toccato dall'ultima compilazione, non è necessario ricompilarlo, si tiene buono il vecchio modulo oggetto compilato al giro precedente. Alla stessa maniera, se viene modificato un dettaglio implementativo di una classe (che sta nel .cpp e non nel .h) non è necessario ricompilare tutti gli altri .cpp che usano quella classe - basta solo ricompilare il .cpp in cui è effettivamente scritto.

Quando si scrive una classe completamente in un .h (quindi con tutti i metodi inline) la compilazione di chi lo include è più lenta (perché deve essere ricompilata l'implementazione della classe ogni volta) e ogni volta che tocchi qualcosa della classe in questione devi ricompilare più o meno tutto. Questo in genere non è un problema se il progetto è piccolo, ma se hai un progetto di qualche centinaio di migliaia di righe di codice che ci mette diversi minuti a compilare (come quelli su cui lavoro tutti i giorni) o un mastodonte come Chrome o LibreOffice - che consistono di decine di milioni di righe di codice, e tempi di compilazione in proporzione - ovviamente vuoi minimizzare il numero di file da ricompilare ad ogni modifica.

Purtroppo il sistema ad include è estremamente primitivo; dato che le possibilità che ha un .h di alterare la semantica di quello che segue dopo l'inclusione sono pressoché infinite (dato che l'#include agisce come un brutale copia-incolla testuale), i tool di sviluppo che si usano normalmente calcolano le dipendenze dei .cpp dai file che includono, e se uno qualunque dei file inclusi cambia in qualunque maniera, il file .cpp viene marcato come "da ricompilare". Questo ovviamente non è ottimale, dato che anche solo una modifica cosmetica (aggiungere un commento, cambiare la spaziatura) ad un file incluso da tutti nel progetto fa sì che venga ricompilato tutto, anche se la modifica di per sé non impatta alcunché dal punto di vista funzionale. Ci sono meccanismi per cercare di mitigare questo problema (ad esempio in VC++ l'opzione "minimal rebuild" (https://stackoverflow.com/a/36898429/214671)), ma sono meccanismi piuttosto delicati e con i loro drawbacks (https://stackoverflow.com/q/49870753/214671).

Inoltre, sempre dato che l'include è un meccanismo puramente testuale, per ogni translation unit in cui un .h viene incluso deve essere ri-parsato da capo (visto che la sua interpretazione può essere influenzata da ciò che precede l'include), con conseguente perdita di tempo.

Per questo, come spiegato da shodan, si sta cercando di standardizzare un meccanismo di moduli, che lavori ad un livello più alto; come avviene per altri linguaggi, una volta che un modulo è compilato questo esporterà le informazioni necessarie al compilatore per usarlo da un'altra translation unit, ma:
- esportando i dati in un formato più comodo di quello puramente testuale degli header (la grammatica del C++ è un disastro da parsare);
- esportando solo funzioni, classi e altri concetti di alto livello, non direttive del preprocessore, macro, commenti e similari; questo evita che ci sia un accoppiamento così stretto tra .cpp e .h che include, con i problemi spiegati sopra che questo comporta.

Loading