Domanda

Come faresti a convertire una base di codice C ragionevolmente grande (> 300K), abbastanza matura in C ++?

Il tipo di CI in mente è suddiviso in file approssimativamente corrispondenti ai moduli (cioè meno granulari di una tipica decomposizione basata su classe OO), usando il collegamento interno al posto di funzioni e dati privati ??e un collegamento esterno per funzioni e dati pubblici . Le variabili globali sono ampiamente utilizzate per la comunicazione tra i moduli. È disponibile una suite di test di integrazione molto ampia, ma nessun test a livello di unità (ad es. Modulo).

Ho in mente una strategia generale:

  1. Compilare tutto nel sottoinsieme C di C ++ e farlo funzionare.
  2. Converti i moduli in enormi classi, in modo che tutti i riferimenti incrociati siano definiti da un nome di classe, ma lasciando tutte le funzioni e i dati come membri statici, e funzionino.
  3. Converti enormi classi in istanze con costruttori appropriati e riferimenti incrociati inizializzati; sostituire gli accessi di membri statici con accessi indiretti, a seconda dei casi; e farlo funzionare.
  4. Ora, avvicina il progetto come un'applicazione OO mal fatturata e scrivi test unitari in cui le dipendenze sono trattabili e decompone in classi separate dove non lo sono; l'obiettivo qui sarebbe quello di passare da un programma di lavoro all'altro ad ogni trasformazione.

Ovviamente, sarebbe un bel po 'di lavoro. Ci sono casi studio / storie di guerra là fuori su questo tipo di traduzione? Strategie alternative? Altri consigli utili?

Nota 1: il programma è un compilatore e probabilmente milioni di altri programmi si basano sul fatto che il suo comportamento non cambia, quindi la riscrittura all'ingrosso non è praticamente un'opzione.

Nota 2: la fonte ha circa 20 anni e ha forse il 30% di abbandono del codice (righe modificate + aggiunte / righe totali precedenti) all'anno. È pesantemente mantenuto ed esteso, in altre parole. Pertanto, uno degli obiettivi sarebbe quello di aumentare la gestibilità.

[Per il bene della domanda, supponi che la traduzione in C ++ sia obbligatoria e che lasciarla in C sia non un'opzione. Il punto di aggiungere questa condizione è quello di eliminare il "lasciarlo in C" risposte.]

È stato utile?

Soluzione

Ho appena iniziato praticamente la stessa cosa alcuni mesi fa (su un progetto commerciale di dieci anni, originariamente scritto con il "C ++" non è altro che C con struct s " filosofia), suggerirei di usare la stessa strategia che avresti usato per mangiare un elefante: prendilo un boccone alla volta. : -)

Per quanto possibile, suddividilo in fasi che possono essere eseguite con effetti minimi su altre parti. Costruire un sistema di facciata, come Federico Ramponi è un buon inizio - una volta che tutto ha una facciata C ++ e comunica attraverso di essa, puoi cambiare gli interni dei moduli con la certezza che non possono influenzare nulla al di fuori di essi.

Avevamo già installato un sistema di interfaccia C ++ parziale (a causa di precedenti piccoli sforzi di refactoring), quindi questo approccio non è stato difficile nel nostro caso. Una volta che tutto comunicava come oggetti C ++ (che ha richiesto alcune settimane, lavorando su un ramo di codice sorgente completamente separato e integrando tutte le modifiche al ramo principale man mano che venivano approvate), raramente non potevamo compilare un totale versione funzionante prima di partire per la giornata.

Il passaggio non è ancora completo: ci siamo fermati due volte per le versioni provvisorie (puntiamo a una versione puntuale ogni poche settimane), ma è sulla buona strada e nessun cliente si è lamentato di eventuali problemi . Anche i nostri addetti al controllo qualità hanno riscontrato un solo problema che ricordo. : -)

Altri suggerimenti

Che dire di:

  1. Compilando tutto nel sottoinsieme C di C ++ e farlo funzionare, e
  2. Implementazione di una serie di facciate lasciando inalterato il codice C?

Perché è obbligatoria la "traduzione in C ++"? Puoi racchiudere il codice C senza il fastidio di convertirlo in enormi classi e così via.

La tua applicazione ha un sacco di gente che ci lavora e ha bisogno di non essere rotta. Se sei serio sulla conversione su larga scala in uno stile OO, cosa hai bisogno di enormi strumenti di trasformazione per automatizzare il lavoro.

L'idea di base è quella di designare gruppi di dati come classi e quindi ottenere lo strumento per il refactoring del codice per spostare tali dati in classi, spostare le funzioni solo su quei dati in quelle classi, e rivedere tutti gli accessi a tali dati per le chiamate alle classi.

Puoi eseguire una preanalisi automatica per formare gruppi di statistiche per ottenere alcune idee, ma avrai comunque bisogno di un ingegnere consapevole dell'applicazione per decidere cosa gli elementi di dati dovrebbero essere raggruppati.

Uno strumento in grado di eseguire questa attività è il nostro Reengineering del software DMS Toolkit. DMS ha potenti parser C per la lettura del codice, acquisisce il codice C. come alberi di sintassi astratti del compilatore, (e diversamente da un compilatore convenzionale) è in grado di calcolare analisi di flusso su tutto il tuo SLOC da 300K. DMS ha un front-end C ++ che può essere utilizzato come "indietro" fine; uno scrive le trasformazioni che associano la sintassi C alla sintassi C ++.

Dà un'importante attività di reingegnerizzazione C ++ su un grande sistema avionico qualche idea di come sia usare DMS per questo tipo di attività. Vedi i documenti tecnici su www.semdesigns.com/Products/DMS/DMSToolkit.html, specificamente Riprogettazione dei modelli di componenti C ++ tramite trasformazione automatica del programma

Questo processo non è per i deboli di cuore. Ma di chiunque altro che considererebbe il refactoring manuale di una grande applicazione non ha già paura del duro lavoro.

Sì, sono associato all'azienda, essendo il suo principale architetto.

Scriverei classi C ++ sull'interfaccia C. Non toccare il codice C diminuirà la possibilità di incasinare e accelerare in modo significativo il processo.

Dopo aver installato l'interfaccia C ++; allora è un compito banale di copiare + incollare il codice nelle tue classi. Come hai già detto, durante questo passaggio è fondamentale eseguire test unitari.

GCC è attualmente in fase di transizione verso C ++ da C. Hanno iniziato spostando tutto nel sottoinsieme comune di C e C ++, ovviamente. Mentre lo facevano, hanno aggiunto avvisi a GCC per tutto ciò che hanno trovato, trovato in -Wc ++ - compat . Questo dovrebbe portarti nella prima parte del tuo viaggio.

Per le ultime parti, una volta che hai effettivamente compilato tutto con un compilatore C ++, mi concentrerei sulla sostituzione di cose che hanno controparti idiomatiche C ++. Ad esempio, se stai usando elenchi, mappe, set, bitvector, hashtable, ecc., Che sono definiti utilizzando macro C, probabilmente otterrai molto spostandoli in C ++. Allo stesso modo con OO, probabilmente troverai vantaggi laddove stai già utilizzando un linguaggio C OO (come l'ereditarietà della struttura) e dove C ++ offrirà maggiore chiarezza e una migliore verifica del tipo sul tuo codice.

La tua lista sembra a posto, tranne che suggerirei di rivedere prima la suite di test e di provare a renderla il più stretta possibile prima di fare qualsiasi codifica.

Lanciamo un'altra stupida idea:

  1. Compilare tutto nel sottoinsieme C di C ++ e farlo funzionare.
  2. Inizia con un modulo, convertilo in una classe enorme, quindi in un'istanza e crea un'interfaccia C (identica a quella da cui sei partito) da quell'istanza. Lascia che il codice C rimanente funzioni con quell'interfaccia C.
  3. Rifattorizza secondo necessità, facendo crescere il sottosistema OO dal codice C un modulo alla volta e rilascia parti dell'interfaccia C quando diventano inutili.

Probabilmente due cose da considerare oltre a come vuoi iniziare sono su cosa vuoi mettere a fuoco e dove vuoi fermarti .

Dichiari che esiste una grande quantità di codice, questa potrebbe essere la chiave per focalizzare i tuoi sforzi. Ti suggerisco di scegliere le parti del tuo codice dove è necessaria molta manutenzione, le parti mature / stabili sembrano funzionare abbastanza bene, quindi è meglio lasciarle così come sono, tranne probabilmente per alcune vetrine con facciate ecc.

Il punto in cui si desidera interrompere dipende dal motivo per cui si desidera convertire in C ++. Questo non può certo essere un obiettivo in sé. Se è dovuto a una dipendenza di terze parti, concentra i tuoi sforzi sull'interfaccia verso quel componente.

Il software su cui lavoro è un'enorme e antica base di codice che è stata "convertita" da C a C ++ anni fa. Penso che sia stato perché la GUI è stata convertita in Qt. Anche adesso sembra quasi sempre un programma C con classi. Rompere le dipendenze causate dai membri dei dati pubblici e riformattare le enormi classi con metodi mostruosi procedurali in metodi più piccoli e le classi non è mai decollato, penso per i seguenti motivi:

  1. Non è necessario modificare il codice che funziona e che non deve essere migliorato. In questo modo vengono introdotti nuovi bug senza aggiungere funzionalità e gli utenti finali non lo apprezzano;
  2. È molto, molto difficile fare il refactor in modo affidabile. Molti pezzi di codice sono così grandi e anche così vitali che le persone difficilmente osano toccarlo. Abbiamo una serie abbastanza ampia di test funzionali, ma è difficile ottenere sufficienti informazioni sulla copertura del codice. Di conseguenza, è difficile stabilire se esistono già prove sufficienti per rilevare i problemi durante il refactoring;
  3. Il ROI è difficile da stabilire. L'utente finale non trarrà vantaggio dal refactoring, quindi deve avere un costo di manutenzione ridotto, che inizialmente aumenterà perché con il refactoring si introducono nuovi bug in codice maturo, ovvero un codice abbastanza privo di bug. E anche il refactoring sarà costoso ...

NB. Suppongo che tu sappia che "Funziona efficacemente con il codice Legacy" prenotare?

Dici che il tuo strumento è un compilatore e che: "In realtà, la corrispondenza dei modelli, non solo la corrispondenza dei tipi, nella spedizione multipla sarebbe ancora migliore".

Potresti dare un'occhiata a maketea . Fornisce la corrispondenza dei modelli per AST, così come la definizione AST da una grammatica astratta e visitatori, trasformatori, ecc.

Se hai un progetto piccolo o accademico (diciamo, meno di 10.000 righe), una riscrittura è probabilmente l'opzione migliore. Puoi fattorizzarlo come vuoi e non ci vorrà troppo tempo.

Se hai un'applicazione del mondo reale, suggerirei di farlo compilare come C ++ (che di solito significa principalmente riparare prototipi di funzioni e simili), quindi lavorare sul refactoring e sul wrapping OO. Naturalmente, non sottoscrivo la filosofia secondo cui il codice deve essere strutturato OO per essere accettabile codice C ++. Farei una conversione pezzo per pezzo, riscrivendo e refactoring come è necessario (per funzionalità o per incorporare test di unità).

Ecco cosa farei:

  • Poiché il codice ha 20 anni, eliminare l'analizzatore parser / sintassi e sostituirlo con uno dei più recenti codici C ++ basati su lex / yacc / bison (o qualcosa di simile) ecc., molto più gestibile e più facile da capire. Anche più veloce da sviluppare se hai un BNF a portata di mano.
  • Una volta che questo è stato adattato al vecchio codice, inizia a avvolgere i moduli in classi. Sostituisci le variabili globali / condivise con le interfacce.
  • Ora quello che hai sarà un compilatore in C ++ (non del tutto).
  • Disegna un diagramma di classe di tutte le classi nel tuo sistema e guarda come stanno comunicando.
  • Disegnane un altro usando le stesse classi e guarda come dovrebbero comunicare.
  • Rifattorizza il codice per trasformare il primo diagramma nel secondo. (potrebbe essere complicato e complicato)
  • Ricorda di usare il codice C ++ per tutto il nuovo codice aggiunto.
  • Se ti rimane del tempo, prova a sostituire le strutture dati una per una per utilizzare il più standard STL o Boost.
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top