Credo di aver mischiato codice C e C ++ quando non avrei dovuto; È un problema e come rettificare?


10

Background / Scenario

Ho iniziato a scrivere un'applicazione CLI esclusivamente in C (il mio primo programma C o C ++ corretto che non era "Hello World" o una sua variante). Verso metà strada stavo lavorando con "stringhe" di input dell'utente (array di caratteri) e ho scoperto l'oggetto streamer di stringhe C ++. Ho visto che potevo salvare il codice usando questi, quindi li ho usati attraverso l'applicazione. Ciò significa che ho modificato l'estensione del file in .cpp e ora compilo l'app g++invece di gcc. Quindi, basandomi su questo, direi che l'applicazione è ora tecnicamente un'applicazione C ++ (sebbene il 90% + del codice sia scritto in quello che chiamerei C, dato che c'è un sacco di incroci tra le due lingue data la mia esperienza limitata di il due). È un singolo file .cpp lungo circa 900 righe.

Fattori importanti

Voglio che il programma sia gratuito (come in denaro) e liberamente distribuibile e utilizzabile da tutti. La mia preoccupazione è che qualcuno guarderà il codice e penserà qualcosa sull'effetto di:

Oh guarda la codifica, è terribile, questo programma non può aiutarmi

Quando potenzialmente potrebbe! Un'altra questione è l'efficienza del codice (è un programma per testare la connettività Ethernet). Non dovrebbero esserci parti del codice così inefficienti da ostacolare gravemente le prestazioni dell'applicazione o del suo output. Tuttavia, penso che sia una domanda per Stack Overflow quando si chiede aiuto con funzioni, metodi, chiamate a oggetti specifici, ecc.

La mia domanda

Avere (secondo me) mescolato C e C ++ dove forse non avrei dovuto. Dovrei cercare di riscrivere tutto in C ++ (con questo, intendo implementare più oggetti e metodi C ++ in cui forse ho codificato qualcosa in uno stile C che può essere condensato usando le nuove tecniche C ++), o rimuovere l'uso di oggetti streamer stringa e riportare tutto "indietro" al codice C? C'è un approccio corretto qui? Mi sono perso e ho bisogno di una guida su come mantenere questa applicazione "buona" agli occhi delle masse, così la useranno e ne trarranno beneficio.

Il codice - Aggiornamento

Ecco un link al codice. Sono circa il 40% dei commenti, commento quasi ogni riga finché non mi sento più fluente. Nella copia a cui mi sono collegato, ho rimosso praticamente tutti i commenti. Spero che questo non lo renda troppo difficile da leggere. Spero comunque che nessuno dovrebbe aver bisogno di capirlo appieno. Se ho commesso errori di progettazione fatali, spero che debbano essere facilmente identificabili. Dovrei anche menzionare, sto scrivendo un paio di desktop e laptop Ubuntu. Non intendo trasferire il codice su altri sistemi operativi.


3
Ho chiarito la CLI per te. CLI può anche fare riferimento a Common Language Infrastructure, che non ha molto senso dal punto di vista C.
Robert Harvey,

Potresti riscriverlo in FORTRAN. Non ho mai sentito parlare di OOF.
ott--

2
Non mi preoccuperei troppo delle persone che non usano il tuo software se non è "carino". Il 99% dei tuoi utenti non guarderà nemmeno il codice o si preoccuperà di come è scritto, purché compia ciò di cui ha bisogno. E ', tuttavia, importante che il codice sia coerente, ecc per aiutare voi nel suo mantenimento a lungo termine.
Evicatos,

1
Perché non rendi il tuo software gratuito (ad es. Con licenza GPLv3 ), con il codice ad es . Github . Nel tuo codice manca un LICENSEfile. Potresti ricevere un feedback interessante.
Basile Starynkevitch,

Risposte:


13

Cominciamo dall'inizio: il codice misto C e C ++ è abbastanza comune. Quindi sei in un grande club per cominciare. Abbiamo enormi basi di codice C in natura. Ma per ovvie ragioni molti programmatori si rifiutano di scrivere almeno nuove cose in C, avendo accesso a C ++ nello stesso compilatore, i nuovi moduli iniziano a essere scritti in questo modo - inizialmente lasciando solo le parti esistenti.

Quindi alla fine alcuni file esistenti vengono ricompilati come C ++ e alcuni bridge possono essere eliminati ... Ma potrebbe richiedere molto tempo.

Sei un po 'avanti, il tuo sistema completo ora è C ++, solo la maggior parte di esso è scritto "C-style". E vedi il mix di stili un problema, cosa che non dovresti: C ++ è un linguaggio multi-paradigma che supporta molti stili e permette loro di coesistere per sempre. In realtà questa è la forza principale, che non sei costretto a un solo stile. Uno che sarebbe subottimale qua e là, con un po 'di fortuna non dappertutto.

Rielaborare la base di codice è una buona idea, SE è rotta. O se è in via di sviluppo. Ma se funziona (nel senso originale della parola), segui il principio ingegneristico di base: se non è rotto, non aggiustarlo. Lascia in pace le parti fredde, metti lo sforzo dove conta. Sulle parti che sono cattive, pericolose - o con nuove funzionalità, e semplicemente rifattorizza le parti per renderle un letto.

Se cerchi cose generali da affrontare, ecco cosa vale la pena sfrattare da una base di codice C:

  • tutte le funzioni str * e char [] - sostituiscile con una classe di stringhe
  • se usi sprintf, crea una versione che restituisca una stringa con il risultato o la inserisca nella stringa e sostituisca l'utilizzo. (Se non ti sei mai disturbato con i flussi fai un favore a te stesso e saltali, a meno che non ti piacciano; gcc offre una perfetta sicurezza di tipo pronta per il controllo dei formati, aggiungi l'attributo giusto.
  • la maggior parte malloc e gratis - NON con nuovo ed elimina, ma vettoriale, elenco, mappa e altri collettori.
  • il resto della gestione della memoria (dopo i due punti precedenti deve essere piuttosto raro, coprire con puntatori intelligenti o implementare le tue raccolte speciali
  • sostituisci tutti gli altri usi delle risorse (FILE *, mutex, lock, ecc.) per usare wrapper o classi RAII

Quando hai finito, ti avvicini al punto in cui la base di codice può essere ragionevolmente sicura dalle eccezioni, quindi puoi eliminare il calcio del codice di ritorno usando eccezioni e rari tentativi di cattura solo nelle funzioni di alto livello.

Oltre a ciò basta scrivere un nuovo codice in un C ++ sano e, se nascono alcune classi che sono un buon sostituto nel codice esistente, raccoglierle.

Non ho menzionato cose relative alla sintassi, ovviamente uso refs invece di puntatori in tutto il nuovo codice, ma sostituire vecchie parti C solo per quella modifica non ha valore. Casta a cui devi indirizzare, eliminare tutto ciò che puoi e utilizzare le varianti C ++ nelle funzioni wrapper per il resto. E, soprattutto, aggiungi const ovunque applicabile. Questi si alternano con i proiettili precedenti. Consolida le tue macro e sostituisci ciò che puoi trasformare in enum, inline function o template.

Suggerisco di leggere gli standard di codifica C ++ di Sutter / Alexandrescu se non ancora fatto e seguirli attentamente.


Grazie mille per l'input Balog, tutti i consigli sonori nei miei occhi e sono d'accordo con tutto questo. Cercherò di cambiare lentamente la sezione del codice in base alla sezione, dando la priorità al codice di lavoro.
jwbensley,

7

In breve: non andrai all'inferno, non accadrà nulla di brutto, ma non vincerai nemmeno gare di bellezza.

Mentre è perfettamente possibile usare C ++ come "migliore C", stai buttando via molti dei vantaggi di C ++, ma poiché non ti stai limitando alla C vaniglia, non stai ottenendo nessuno dei vantaggi di C (semplicità, portabilità , trasparenza, interoperabilità). In altre parole: C ++ sacrifica alcune delle qualità di C per ottenerne altre - per esempio, la trasparenza di C dove puoi sempre vedere chiaramente dove e quando avvengono le allocazioni di memoria viene scambiata per le astrazioni più potenti di C ++.

Dal momento che il tuo codice sembra funzionare bene ora, probabilmente non è una buona idea riscriverlo solo per il gusto di farlo: mantienilo com'è per ora e cambialo in C ++ più idiomatico mentre procedi, un pezzo alla volta, ogni volta che lavori su una determinata parte. E tieni a mente le lezioni così apprese per il tuo prossimo progetto, soprattutto questo: C e C ++ non sono la stessa lingua, e farai meglio a scegliere in anticipo piuttosto che decidere di passare a C ++ a metà del tuo progetto .


Grazie tdammers per il consiglio. Sono d'accordo con quello che stai dicendo e lo prendo a bordo, grazie!
jwbensley,

4

Il C ++ è un linguaggio molto complesso, nel senso che ha molte funzionalità. È anche un linguaggio che supporta molteplici paradigmi, il che significa che consente di scrivere codice interamente procedurale senza utilizzare alcun oggetto.

Quindi, poiché il C ++ è così complesso, spesso vedi che le persone usano un sottoinsieme limitato delle sue funzionalità nel loro codice. I principianti spesso usano semplicemente l'I / O stream, gli oggetti stringa e new / delete invece di malloc / free e forse riferimenti invece di puntatori. Man mano che apprendi le funzionalità orientate agli oggetti, puoi iniziare a scrivere in uno stile chiamato "C con classi". Alla fine, mentre impari di più sul C ++, inizi a utilizzare modelli, RAII , STL , puntatori intelligenti, ecc.

Il punto che sto cercando di sottolineare è che l'apprendimento del C ++ è un processo che richiede tempo. Sì, in questo momento probabilmente il tuo codice sembra essere stato scritto da un programmatore C che cerca di scrivere C ++. E dato che stai solo imparando, va benissimo. Tuttavia, mi fa rabbrividire, quando vedo questo genere di cose nel codice di produzione scritto da programmatori esperti che dovrebbero conoscere meglio.

Ricorda, un buon programmatore Fortran può scrivere un buon codice Fortran in qualsiasi lingua. :)


2

Sembra che tu stia ricapitolando esattamente il percorso che molti vecchi programmatori C hanno intrapreso quando vanno in C ++. Lo stai usando come una "C migliore". Venti anni fa questo aveva un senso, ma a questo punto ci sono così tanti costrutti più potenti in C ++ (come la Standard Template Library) che raramente ha senso ora. Come minimo, probabilmente dovresti fare quanto segue per evitare di dare aneurismi ai programmatori C ++ quando guardi il tuo codice:

  • Usa i flussi anziché il? printffamiglia.
  • Usa newinvece di malloc.
  • Utilizzare le classi del contenitore STL per tutte le strutture di dati.
  • Utilizzare std::stringinvece che char*dove possibile
  • Comprendi e usa RAII

Se il tuo programma è breve (e ~ 900 righe sembrano brevi) Personalmente non penso che sia necessario o addirittura utile cercare di creare un solido set di classi.


Grazie per il consiglio Steven, Sì, avevo la stessa idea sulle classi e penso di poter riordinare il codice in un unico file ordinato. Grazie!
jwbensley,

2

La risposta accettata menziona solo i professionisti della conversione da C a C ++ idiomatico, come se il codice C ++ fosse in un certo senso migliore del codice C. Concordo con altre risposte sul fatto che probabilmente non è necessario apportare modifiche drastiche al codice misto se non è rotto.

Se la miscelazione di C e C ++ è sostenibile dipende da come viene eseguita la miscelazione. Bug sottili possono essere introdotti, ad esempio, quando vengono sollevate eccezioni nel codice C (che non è sicuro), causando perdite di memoria o corruzione dei dati. Un modo più sicuro e abbastanza comune è avvolgere le librerie C o le interfacce nelle classi o usarle in qualche altro tipo di isolamento in un progetto C ++.

Prima di decidere di riscrivere l'intero progetto in C ++ idiomatico (o C), è necessario essere consapevoli del fatto che molte delle modifiche presentate in altre risposte possono rallentare il programma o causare altri effetti indesiderati. Per esempio:

  • la modifica delle stringhe C allocate in pila in std :: stringhe può causare allocazioni di heap non necessarie
  • la modifica dei puntatori non elaborati in alcuni tipi di puntatori condivisi (come std :: shared_ptr) provoca un sovraccarico di accesso a causa del conteggio dei riferimenti, delle funzioni interne dei membri virtuali e della sicurezza dei thread
  • i flussi di libreria standard sono più lenti rispetto alle controparti C.
  • un uso negligente delle classi RAII, come i container, può causare operazioni non necessarie, soprattutto quando non è possibile utilizzare la semantica di spostamento di C ++ 11
  • i modelli possono causare tempi di compilazione più lunghi, errori di compilazione poco chiari, ingombro di codice e problemi di portabilità tra i compilatori
  • scrivere un codice sicuro per le eccezioni è difficile, il che rende semplice l'introduzione di bug sottili durante la conversione
  • è più complicato utilizzare il codice C ++ da una libreria condivisa rispetto al semplice C
  • C ++ non è ampiamente supportato come ad esempio C89

Per concludere, la conversione da codice C e C ++ misto a C ++ idiomatico dovrebbe essere vista come un compromesso che ha molto di più rispetto al semplice tempo di implementazione e all'ampliamento della cassetta degli attrezzi con funzionalità apparentemente convenienti. Pertanto è molto difficile dare una risposta per il caso generale diverso da "dipende".


"I flussi di libreria standard sono più lenti delle controparti C": Probabilmente, sicuramente più difficile per l'internazionalizzazione rispetto a printf.
Deduplicatore,

1

È una cattiva forma mescolare C e C ++. Sono lingue distinte e dovrebbero davvero essere trattate come tali. Scegli quello con cui ti senti più a tuo agio e prova a scrivere un codice idiomatico in quella lingua.

Se C ++ ti sta risparmiando un sacco di codice, segui C ++ e riscrivi le parti C. Dubito che una differenza di prestazione sarà persino evidente, se questo è ciò che ti preoccupa.


Quindi ho questa libreria che voglio usare che è scritta in C, e ho un'altra libreria che voglio usare che è scritta in C ++ ... Riscrivere il codice che funziona bene sta solo creando lavoro non necessario.
gnasher729,

1

Vado avanti e indietro tra C e C ++ tutto il tempo e sono il tipo strano che preferisce lavorare in questo modo. Preferisco il C ++ per cose di livello superiore mentre uso C come uno strumento smussato che mi consente di radiografare i tipi di dati e di trattarli come bit e byte con, diciamo, memcpyche trovo utili per l'implementazione di strutture di dati di basso livello e allocatori di memoria . Se ti ritrovi a lavorare a livello di bit e byte grezzi, il sistema di tipo veramente ricco di C ++ non aiuta affatto nulla e spesso trovo più facile scrivere un codice del genere in C dove posso tranquillamente supporre che posso considera qualsiasi tipo di dati C come solo bit e byte.

La cosa principale da tenere a mente è dove queste due lingue non si adattano bene.

1. La funzione AC non dovrebbe mai chiamare una funzione C ++ che può throw. Dato il numero di posizioni in cui il tuo tipo quotidiano di codice C ++ può implicitamente incorrere in un'eccezione, ciò significa generalmente che le tue funzioni C non dovrebbero chiamare funzioni C ++ in generale. In caso contrario, il codice C non sarà in grado di liberare risorse allocate durante lo svolgimento dello stack poiché non ha un modo pratico per rilevare l'eccezione C ++. Se si finisce per richiedere che il proprio codice C richiami una funzione C ++ come callback, ad esempio, il lato C ++ dovrebbe assicurarsi che qualsiasi eccezione riscontrata venga rilevata prima di tornare al codice C.

2. Se scrivi il codice C che tratta i tipi di dati solo come bit e byte grezzi, facendo bulldoking sul sistema dei tipi (questo è in realtà il mio motivo principale per usare C in primo luogo), allora non vorrai mai usare tale codice con i dati C ++ tipi che potrebbero avere costruttori e distruttori di copie e puntatori virtuali e cose del genere che devono essere rispettati. Quindi generalmente dovrebbe essere il codice C usando il codice C di questo tipo, non il codice C ++ usando il codice C di questo tipo.

Se si desidera che il proprio codice C ++ utilizzi tale codice C, in genere si desidera utilizzarlo come dettaglio di implementazione di una classe che si accerta che i dati memorizzati nella struttura di dati C generica siano un tipo di dati banale da costruire e distruggere. Fortunatamente ci sono tratti del tipo come questo che puoi usare per verificarlo in un'asserzione statica, assicurandoti che i tipi che stai memorizzando nella struttura di dati C generica abbiano distruttori e costruttori banali e rimarranno tali.

Dovrei cercare di riscrivere tutto in C ++ (con questo, intendo implementare più oggetti e metodi C ++ in cui forse ho codificato qualcosa in uno stile C che può essere condensato usando le nuove tecniche C ++), o rimuovere l'uso di oggetti streamer stringa e riportare tutto "indietro" al codice C?

Secondo me non devi preoccuparti se aderisci alle regole sopra e assicurati che il tuo codice sia ben testato rispetto ai test che hai scritto. La parte che potrebbe valere la pena migliorare è il codice che hai scritto finora in C ++, ma ciò non significa che devi eseguire il porting del codice strettamente basato su C ++.

Con il codice che hai scritto in C ++ e compila come codice C ++, devi fare attenzione. L'uso della codifica C-like in C ++ richiede problemi. Ad esempio, se si allocano e si liberano risorse manualmente, è probabile che il codice non sia sicuro dalle eccezioni poiché il codice potrebbe riscontrare un'eccezione a quel punto si esce implicitamente dalla funzione prima di poter mai utilizzare freela risorsa. In C ++, RAII e cose come i puntatori intelligenti non sono una comodità soffice in quanto potrebbero apparire se si guardassero solo i normali percorsi di esecuzione senza considerare percorsi eccezionali. Spesso sono una necessità fondamentale per scrivere il codice corretto e sicuro per le eccezioni in modo semplice ed efficace che non inizi a perdere in tutto il luogo quando si incontra un'eccezione.

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.