Quali sono i vantaggi e gli svantaggi della separazione di dichiarazione e definizione come in C ++?

StackOverflow https://stackoverflow.com/questions/645778

Domanda

In C ++, la dichiarazione e la definizione di funzioni, variabili e costanti possono essere separate in questo modo:

function someFunc();

function someFunc()
{
  //Implementation.
}

In effetti, nella definizione delle classi, questo è spesso il caso. Una classe viene generalmente dichiarata con i suoi membri in un file .h e questi vengono quindi definiti in un corrispondente file .C.

Quali sono i vantaggi & amp; svantaggi di questo approccio?

È stato utile?

Soluzione

Storicamente questo era di aiuto al compilatore. Dovevi dargli l'elenco dei nomi prima che li usasse - se si trattava dell'uso effettivo o di una dichiarazione diretta (a parte il prototipo della funzione predefinita di C).

I compilatori moderni per le lingue moderne mostrano che questa non è più una necessità, quindi C & amp; La sintassi di C ++ (così come Objective-C, e probabilmente altri) qui è un bagaglio istotico. In effetti uno di questi è uno dei maggiori problemi con C ++ che nemmeno l'aggiunta di un adeguato sistema di moduli risolverà.

Gli svantaggi sono: molti file di inclusione pesantemente nidificati (ho tracciato includendo alberi prima, sono sorprendentemente enormi) e ridondanza tra dichiarazione e definizione - tutto ciò porta a tempi di codifica più lunghi e tempi di compilazione più lunghi (mai confrontati i tempi di compilazione tra progetti comparabili C ++ e C #? Questo è uno dei motivi della differenza). I file di intestazione devono essere forniti agli utenti dei componenti forniti. Possibilità di violazioni ODR. Affidamento al pre-processore (molte lingue moderne non richiedono un passaggio pre-processore), il che rende il codice più fragile e più difficile da analizzare per gli strumenti.

Vantaggi: non molto. Si potrebbe sostenere che si ottiene un elenco di nomi di funzioni raggruppati in un unico posto a scopo di documentazione - ma la maggior parte degli IDE ha una sorta di capacità di ripiegamento del codice in questi giorni e progetti di qualsiasi dimensione dovrebbero comunque utilizzare generatori di documenti (come doxygen). Con una sintassi più pulita, priva di processori e basata su modulo, è più facile per gli strumenti seguire il codice e fornire questo e altro, quindi penso che questo "vantaggio" è solo discutibile.

Altri suggerimenti

È un artefatto di come funzionano i compilatori C / C ++.

Man mano che viene compilato un file sorgente, il preprocessore sostituisce ogni # include-statement con il contenuto del file incluso. Solo in seguito il compilatore tenta di interpretare il risultato di questa concatenazione.

Il compilatore quindi analizza quel risultato dall'inizio alla fine, cercando di convalidare ogni istruzione. Se una riga di codice invoca una funzione che non è stata definita in precedenza, si arrenderà.

C'è un problema in questo, però, quando si tratta di chiamate di funzione reciprocamente ricorsive:

void foo()
{
  bar();
}

void bar()
{
  foo();
}

Qui foo non verrà compilato poiché bar è sconosciuto. Se si cambiano le due funzioni, bar non verrà compilato poiché foo è sconosciuto.

Se si separano la dichiarazione e la definizione, tuttavia, è possibile ordinare le funzioni come desiderato:

void foo();
void bar();

void foo()
{
  bar();
}

void bar()
{
  foo();
}

Qui, quando il compilatore elabora foo conosce già la firma di una funzione chiamata bar ed è felice.

Naturalmente i compilatori potrebbero funzionare in modo diverso, ma è così che funzionano in C, C ++ e in una certa misura Objective-C.

Svantaggi:

Nessuno direttamente. Se stai usando C / C ++ comunque, è il modo migliore per fare le cose. Se hai una scelta di lingua / compilatore, forse puoi sceglierne uno in cui questo non è un problema. L'unica cosa da considerare con la suddivisione delle dichiarazioni in file di intestazione è evitare # istruzioni-include reciprocamente ricorsive - ma questo è ciò a cui servono le guardie di inclusione.

I vantaggi:

  • Velocità di compilazione: poiché tutti i file inclusi vengono concatenati e quindi analizzati, riducendo la quantità e la complessità del codice nei file inclusi migliorerà i tempi di compilazione.
  • Evita la duplicazione / inclinazione del codice: se definisci completamente una funzione in un file di intestazione, ogni file di oggetto che include questa intestazione e riferimenti a questa funzione conterrà la propria versione di quella funzione. Come nota a margine, se vuoi in linea, devi inserire la definizione completa nel file di intestazione (sulla maggior parte dei compilatori).
  • Incapsulamento / chiarezza: una classe / un insieme di funzioni ben definite oltre ad alcuni documenti dovrebbero essere sufficienti per consentire ad altri sviluppatori di utilizzare il codice. (Idealmente) non è necessario che capiscano come funziona il codice, quindi perché obbligarli a setacciarlo? (La contro argomentazione secondo cui può essere utile per loro accedere all'implementazione quando richiesto è ancora valida, ovviamente).

E, naturalmente, se non sei interessato ad esporre una funzione, di solito puoi comunque scegliere di definirla completamente nel file di implementazione piuttosto che nell'intestazione.

Esistono due vantaggi principali nel separare la dichiarazione e la definizione in file di intestazione e sorgente C ++. Il primo è che eviti problemi con la una regola di definizione quando la tua classe / funzioni / qualunque cosa siano #include d in più di un posto. In secondo luogo, facendo le cose in questo modo, si separano l'interfaccia e l'implementazione. Gli utenti della tua classe o libreria devono solo vedere il tuo file header per scrivere il codice che lo utilizza. Puoi anche fare questo passo oltre con il Pimpl Idiom e renderlo tale utente il codice non deve ricompilarsi ogni volta che l'implementazione della libreria cambia.

Hai già menzionato lo svantaggio della ripetizione del codice tra i file .h e .cpp. Forse ho scritto il codice C ++ per troppo tempo, ma non penso che sia così cattivo. Devi cambiare tutto il codice utente ogni volta che cambi comunque la firma di una funzione, quindi qual è un altro file? È fastidioso solo quando scrivi una classe per la prima volta e devi copiare e incollare dall'intestazione nel nuovo file di origine.

L'altro svantaggio nella pratica è che per scrivere (ed eseguire il debug!) un buon codice che utilizza una libreria di terze parti, di solito devi vedere al suo interno. Ciò significa l'accesso al codice sorgente anche se non è possibile modificarlo. Se tutto ciò che hai è un file header e un file oggetto compilato, può essere molto difficile decidere se il bug è colpa tua o loro. Inoltre, esaminare l'origine consente di comprendere come utilizzare ed estendere correttamente una libreria che la documentazione potrebbe non coprire. Non tutti spediscono un MSDN con la loro libreria. E i grandi ingegneri del software hanno la brutta abitudine di fare cose con il tuo codice che non avresti mai immaginato possibile. ; -)

Lo standard richiede che quando si utilizza una funzione, una dichiarazione deve essere nell'ambito. Ciò significa che il compilatore dovrebbe essere in grado di verificare su un prototipo (la dichiarazione in un file di intestazione) che cosa gli stai passando. Tranne ovviamente, per le funzioni che sono variabili, tali funzioni non convalidano gli argomenti.

Pensa a C, quando questo non era richiesto. A quel tempo, i compilatori non consideravano nessuna specifica del tipo restituito come predefinita a int. Ora, supponiamo che tu abbia una funzione foo () che ha restituito un puntatore a vuoto. Tuttavia, poiché non si dispone di una dichiarazione, il compilatore penserà che debba restituire un numero intero. Su alcuni sistemi Motorola, ad esempio, interi e puntatori verrebbero restituiti in registri diversi. Ora, il compilatore non utilizzerà più il registro corretto e restituirà invece il cast del puntatore a un numero intero nell'altro registro. Nel momento in cui provi a lavorare con questo puntatore, si scatena l'inferno.

Le funzioni di dichiarazione all'interno dell'intestazione vanno bene. Ma ricorda se dichiari e definisci nell'intestazione assicurati che siano in linea. Un modo per ottenere ciò è inserire la definizione all'interno della definizione della classe. Altrimenti anteporre la parola chiave inline . Incontrerai una violazione ODR altrimenti quando l'intestazione è inclusa in più file di implementazione.

Fondamentalmente hai 2 visualizzazioni sulla classe / funzione / qualunque cosa:

La dichiarazione, in cui si dichiarano il nome, i parametri e i membri (nel caso di una struttura / classe) e la definizione in cui si definisce cosa fanno le funzioni.

Tra gli svantaggi ci sono la ripetizione, ma un grande vantaggio è che puoi dichiarare la tua funzione come int foo (float f) e lasciare i dettagli nell'implementazione (= definizione), quindi chiunque voglia per utilizzare la funzione foo include solo il file di intestazione e i collegamenti alla libreria / al file oggetto, quindi gli utenti della libreria e i compilatori devono solo occuparsi dell'interfaccia definita, che aiuta a comprendere le interfacce e accelera i tempi di compilazione.

Un vantaggio che non ho ancora visto: API

Qualsiasi libreria o codice di terze parti che NON è open source (cioè proprietario) non avrà la sua implementazione insieme alla distribuzione. La maggior parte delle aziende è semplicemente a disagio nel dare via il codice sorgente. La soluzione semplice, basta distribuire le dichiarazioni di classe e le firme di funzione che consentono l'uso della DLL.

Disclaimer: non sto dicendo se sia giusto, sbagliato o giustificato, sto solo dicendo che l'ho visto molto.

Advantage

È possibile fare riferimento alle classi da altri file includendo semplicemente la dichiarazione. Le definizioni possono quindi essere collegate in seguito nel processo di compilazione.

Un grande vantaggio delle dichiarazioni a termine è che se usato con attenzione è possibile ridurre le dipendenze dei tempi di compilazione tra i moduli.

Se ClassA.h deve fare riferimento a un elemento di dati in ClassB.h, è spesso possibile utilizzare solo riferimenti diretti in ClassA.h e includere ClassB.h in ClassA.cc anziché in ClassA.h, riducendo così una dipendenza dal tempo di compilazione.

Per i sistemi di grandi dimensioni questo può comportare un notevole risparmio di tempo in una build.

  1. La separazione offre una vista chiara e ordinata degli elementi del programma.
  2. Possibilità di creare e collegarsi a moduli / librerie binarie senza rivelare fonti.
  3. Collega i binari senza ricompilare le fonti.

Se eseguita correttamente, questa separazione riduce i tempi di compilazione quando è cambiata solo l'implementazione.

Svantaggi

Questo porta a molte ripetizioni. La maggior parte della firma della funzione deve essere collocata in due o più (come notato da Paulious).

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top