Pulizia del codice legacy “header spaghetti”

Qualche pratica raccomandata per la pulizia di “header spaghetti” che sta causando tempi di compilazione estremamente lenti (Linux / Unix)?

C’è qualche equivoco a “#pragma once” con GCC?
(trovato messaggi contrastanti riguardo a questo)

Grazie.

Supponendo che tu abbia familiarità con “includi guardie” (#ifdef all’inizio dell’intestazione ..), un ulteriore modo per accelerare i tempi di costruzione è usando le guardie incluse esterne. È stato discusso in ” Progettazione software C ++ su larga scala “. L’idea è che le protezioni classiche includono, a differenza di #pragma una volta, non ti risparmiano l’analisi del preprocessore richiesta per ignorare l’intestazione dalla seconda volta in poi (cioè deve ancora analizzare e cercare l’inizio e la fine della guardia di inclusione. le guardie esterne includono le # ifdef attorno alla linea #include stessa.

Quindi sembra questo:

#ifndef MY_HEADER #include "myheader.h" #endif 

e naturalmente nel file H hai il classico include guard

 #ifndef MY_HEADER #define MY_HEADER // content of header #endif 

In questo modo, il file myheader.h non viene nemmeno aperto / analizzato dal preprocessore e può risparmiare molto tempo in progetti di grandi dimensioni, specialmente quando i file di intestazione si trovano su postazioni remote condivise, come a volte fanno.

di nuovo, è tutto in quel libro. hth

Se si desidera eseguire una pulizia completa e avere il tempo per farlo, la soluzione migliore è eliminare tutti gli #inclusi in tutti i file (ad eccezione di quelli ovvi, ad esempio abc.h in abc.cpp) e quindi compilare il progetto. Aggiungi la necessaria dichiarazione anticipata o intestazione per correggere il primo errore e poi ripeti fino a quando non compili in modo pulito.

Questo non risolve problemi sottostanti che possono comportare problemi di inclusione, ma garantisce che gli unici inclusi siano quelli obbligatori.

Ho letto che GCC considera #pragma once deprecato, anche se #pragma once può fare solo così tanto per velocizzare le cose.

Per cercare di districare gli spaghetti #include , puoi guardare in Doxygen . Dovrebbe essere in grado di generare grafici di intestazioni incluse, che potrebbero darti un vantaggio nel semplificare le cose. Non riesco a ricordare i dettagli a mano libera, ma le funzionalità del grafico potrebbero richiedere l’installazione di GraphViz e indicare a Doxygen il percorso in cui è ansible trovare il dotty.exe di GraphViz.

Un altro approccio che potresti prendere in considerazione se il tempo di compilazione è la tua preoccupazione principale è la configurazione delle intestazioni precompilate .

Ho letto l’altro giorno su un trucco per ridurre le dipendenze dell’intestazione: scrivi uno script che lo farà

  • trova tutte le istruzioni #include
  • rimuovere una dichiarazione alla volta e ricompilare
  • se la compilazione fallisce, aggiungi di nuovo l’istruzione include

Alla fine, si spera che si raggiunga il minimo richiesto incluso nel codice. Potresti scrivere uno script simile che ri-organizza include per scoprire se sono autosufficienti o richiedono che altre intestazioni siano incluse prima di loro (includi prima l’intestazione, vedi se la compilazione fallisce, segnalala). Questo dovrebbe andare in qualche modo a ripulire il tuo codice.

Altre note:

  • I moderni compilatori (gcc tra loro) riconoscono le protezioni di intestazione e si ottimizzano allo stesso modo di una volta, ma aprendo il file una sola volta.
  • pragma una volta può essere problematico quando lo stesso file ha nomi diversi nel filesystem (cioè con soft-link)

  • gcc supporta #pragma una volta, ma lo chiama “obsoleto”
  • pragma una volta non è supportato da tutti i compilatori e non fa parte dello standard C

  • non solo i compilatori possono essere problematici. Strumenti come Incredibuild hanno anche problemi con #pragma una volta

Richard aveva un po ‘ragione (perché la sua soluzione è stata annotata?).

Ad ogni modo, tutte le intestazioni C / C ++ dovrebbero utilizzare le protezioni interne incluse.

Ciò detto, o:

1 – Il tuo codice legacy non è più mantenuto, e dovresti usare header pre-compilati (che sono un hack, ma hey … Il tuo bisogno è di velocizzare la tua compilation, non di refactoring codice non mantenuto)

2 – Il tuo codice legacy è ancora in vita. Quindi, puoi utilizzare le intestazioni precompilate e / o le guardie / guardie esterne per una soluzione temporanea, ma alla fine dovrai rimuovere tutti i tuoi include, uno .C o .CPP alla volta e compilarli. C o .CPP file uno alla volta, correggendo il loro include con forward-declaration o include quando necessario (o anche rompere una grande inclusione in quelli più piccoli per essere sicuri che ogni file .C o .CPP otterrà solo le intestazioni di cui ha bisogno). In ogni caso, testare e rimuovere gli obsoleti include fa parte del mantenimento di un progetto, quindi …

La mia esperienza con le intestazioni precompilate non era esattamente buona, perché metà del tempo, il compilatore non riusciva a trovare un simbolo che avevo definito, e quindi ho provato un “clean / rebuild” completo, per essere sicuro che non fosse l’intestazione precompilata era obsoleto. Quindi la mia ipotesi è di usarlo per librerie esterne che non toccherà nemmeno (come le intestazioni API STL, C, Boost, qualunque cosa). Tuttavia, la mia esperienza è stata con Visual C ++ 6, quindi immagino (spero?) Che abbiano capito bene, ora.

Ora, un’ultima cosa: le intestazioni dovrebbero sempre essere autosufficienti. Ciò significa che se l’inclusione delle intestazioni dipende dall’ordine di inclusione, allora hai un problema. Ad esempio, se puoi scrivere:

 #include "AAA.hpp" #include "BBB.hpp" 

Ma no:

 #include "BBB.hpp" #include "AAA.hpp" 

perché BBB dipende da AAA, quindi tutto ciò che hai è una dipendenza che non hai mai riconosciuto nel codice. Non riconoscerlo con una definizione renderà solo la tua compilation un incubo. BBB dovrebbe includere anche AAA (anche se potrebbe essere un po ‘più lento: alla fine, le dichiarazioni in avanti puliranno comunque gli elementi inutili, quindi dovresti avere un timer di compilazione più veloce).

Utilizzare uno o più di quelli per accelerare il tempo di costruzione

  1. Usa intestazioni precompilate
  2. Utilizzare un meccanismo di memorizzazione nella cache (scons per esempio)
  3. Usa un sistema di build distribuito (distcc, Incredibuild ($))

Nelle intestazioni: includere le intestazioni solo se non è ansible utilizzare la dichiarazione di inoltro, ma sempre # include qualsiasi file necessario (includere le dipendenze sono malvagie!).

Come menzionato nell’altra risposta, dovresti sicuramente usare le dichiarazioni anticipate ogni volta che è ansible. Per quanto ne so, GCC non ha nulla di equivalente a #pragma una volta, motivo per cui mi atteno al vecchio stile di moda delle guardie incluse.

Grazie per le risposte, ma la domanda riguarda il codice esistente che include il rigoroso “include ordine” ecc. La domanda è se ci sono strumenti / script per chiarire cosa sta realmente accadendo.

Le protezioni dell’intestazione non rappresentano la soluzione in quanto non impediscono al compilatore di leggere l’intero file ancora e ancora e …

PC-Lint farà molta strada per ripulire le testate degli spaghetti. Inoltre, risolverà anche altri problemi come le variabili non inizializzate che non vengono visualizzate, ecc.

Come commentato da onebyone.livejournal.com in risposta alla tua domanda, alcuni compilatori supportano l’ ottimizzazione della guardia , che la pagina I linked definisce come segue:

L’ottimizzazione include guard è quando un compilatore riconosce l’idioma di guardia incluso interno descritto sopra e prende provvedimenti per evitare di aprire il file più volte. Il compilatore può guardare un file di inclusione, eliminare i commenti e lo spazio bianco e capire se l’intero file si trova all’interno delle protezioni di inclusione. Se lo è, memorizza il nome file e include la condizione di guardia in una mappa. La prossima volta che viene chiesto al compilatore di includere il file, può controllare la condizione di protezione inclusa e prendere la decisione se saltare il file o # includerlo senza dover aprire il file.

Poi di nuovo, hai già risposto che le guardie esterne incluse non sono la risposta alla tua domanda. Per districare i file di intestazione che devono essere inclusi in un ordine specifico, suggerirei quanto segue:

  • Ogni file .c o .cpp dovrebbe #include prima il file .h corrispondente, e il resto delle sue #include direttive dovrebbero essere ordinate alfabeticamente. Solitamente si generano errori di compilazione quando si rompono le dipendenze non dichiarate tra i file di intestazione.
  • Se hai un file di intestazione che definisce typedef globali per i tipi di base o le direttive #define globali che sono usate per la maggior parte del codice, ogni file .h dovrebbe #include prima quel file, e il resto delle sue #include direttive dovrebbero essere ordinate alfabeticamente .
  • Quando queste modifiche causano errori di compilazione, in genere devi aggiungere una dipendenza esplicita da un file di intestazione a un altro, sotto forma di #include .
  • Quando queste modifiche non causano errori di compilazione, potrebbero causare modifiche comportamentali. Si spera che tu abbia una specie di suite di test che puoi usare per verificare la funzionalità della tua applicazione.

Sembra anche che parte del problema potrebbe essere che le build incrementali sono molto più lente di quanto dovrebbero essere. Questa situazione può essere migliorata con dichiarazioni anticipate o un sistema di build distribuito, come altri hanno sottolineato.