peggiori pratiche in C ++, errori comuni [chiuso]

34

Dopo aver letto questo famoso rant di Linus Torvalds , mi sono chiesto quali siano in realtà tutte le insidie per i programmatori in C ++. Mi riferisco esplicitamente a errori di battitura o cattivi flussi di programma trattati in questa domanda e le sue risposte , ma a più errori di alto livello che non vengono rilevati dal compilatore e non risultano ovvi bug alla prima esecuzione, errori di progettazione completi, cose che sono improbabili in C ma che probabilmente saranno fatte in C ++ dai nuovi arrivati che non capiscono tutte le implicazioni del loro codice.

Inoltre, accolgo con favore le risposte sottolineando un enorme calo delle prestazioni in cui non ci si aspetterebbe di solito. Un esempio di quello che una volta uno dei miei professori mi ha detto di un generatore di parser LR (1) che ho scritto:

You have used somewhat too many instances of unneeded inheritance and virtuality. Inheritance makes a design much more complicated (and inefficient because of the RTTI (run-time type inference) subsystem), and it should therefore only be used where it makes sense, e.g. for the actions in the parse table. Because you make intensive use of templates, you practically don't need inheritance."

    
posta Felix Dombek 07.02.2011 - 21:09
fonte

13 risposte

69

Torvalds sta parlando dal suo culo qui.

OK, perché sta parlando dal suo culo:

Prima di tutto, il suo rant non è proprio niente MA sballo. C'è molto poco contenuto reale qui. L'unica ragione per cui è davvero famosa o anche leggermente rispettata è perché è stata creata dal Dio Linux. La sua argomentazione principale è che C ++ è una schifezza e gli piace pisciare le persone del C ++. Ovviamente non c'è alcun motivo per rispondere a questo e chiunque lo consideri un argomento ragionevole va comunque oltre la conversazione.

Riguardo a ciò che potrebbe essere luccicato come i suoi punti più oggettivi:

  • STL e Boost sono pazzesche ... - qualunque cosa. Sei un idiota.
  • STL e Boost causano infiniti dolori < - ridicolo. Ovviamente è intenzionalmente esagerato, ma qual è la sua vera affermazione qui? Non lo so. C'è qualcosa di più che banalmente difficile da capire quando si provoca il vomito del compilatore in Spirit o qualcosa del genere, ma non è più o meno difficile capire che eseguire il debug di UB causato dall'abuso di costrutti C come void *.
  • I modelli astratti incoraggiati dal C ++ sono inefficienti. < - Come cosa? Non si espande mai, non fornisce mai esempi di ciò che intende, lo dice solo. BFD. Dal momento che non posso dire a cosa si sta riferendo non ha molto senso cercare di "confutare" la dichiarazione. È un comune mantra dei bigotti C, ma ciò non lo rende più comprensibile o comprensibile.
  • L'uso corretto di C ++ significa limitarsi agli aspetti C. < - In realtà il codice WORSE C ++ lo fa così non conosco ancora il WTF di cui parla.

Fondamentalmente, Torvalds sta parlando dal suo culo. Non c'è alcun argomento comprensibile su nulla. Aspettarsi una seria confutazione di tali assurdità è semplicemente sciocco. Mi viene detto di "espandermi" su una confutazione di qualcosa che mi sarei aspettato di espandere se fosse dove io che l'ho detto. Se davvero, onestamente guarda cosa ha detto Torvalds, vedresti che in realtà non ha detto nulla.

Solo perché Dio dice che ciò non significa che abbia alcun senso o che dovrebbe essere preso più sul serio rispetto a un bozo casuale detto. A dire il vero, Dio è solo un altro bozo casuale.

Rispondendo alla domanda attuale:

Probabilmente la peggiore, e più comune, cattiva pratica del C ++ è trattarla come C. L'uso continuato di funzioni API C come printf, gets (anche considerato negativo in C), strtok, ecc ... non solo non riescono a fare leva la potenza fornita dal sistema di tipo più stretto, inevitabilmente portano a ulteriori complicazioni quando si cerca di interagire con il codice C ++ "reale". Quindi, in pratica, fai esattamente l'opposto di quello che consiglia Torvalds.

Impara a sfruttare l'STL e il Boost per ottenere ulteriori rilevamenti dei bug in fase di compilazione e per semplificarti la vita in altri modi generali (il tokenizzatore boost, ad esempio, è sia sicuro per il tipo che un'interfaccia migliore). È vero che dovrai imparare come leggere gli errori dei template, il che è scoraggiante all'inizio, ma (nella mia esperienza comunque) è francamente molto più facile che provare a eseguire il debug di qualcosa che genera un comportamento indefinito durante il runtime, cosa che la C api rende abbastanza facile da fare.

Non dire che C non è buono. Io ovviamente preferisco il C ++. I programmatori C preferiscono C. Ci sono trade off e gusti soggettivi in gioco. C'è anche molta disinformazione e FUD in giro. Direi che c'è più FUD e disinformazione intorno a C ++ ma sono parziale in questo senso. Ad esempio, i problemi "gonfiati" e "performanti" che C ++ ha probabilmente non sono in realtà i problemi principali per la maggior parte del tempo e sono certamente spazzati via dalle proporzioni della realtà.

Per quanto riguarda i problemi a cui si riferisce il tuo professore, questi non sono unici per C ++. In OOP (e in programmazione generica) si preferisce la composizione sull'ereditarietà. L'ereditarietà è la relazione di accoppiamento più strong possibile esistente in tutti i linguaggi OO. Il C ++ ne aggiunge uno più strong, l'amicizia. L'eredità polimorfica dovrebbe essere usata per rappresentare astrazioni e relazioni "is-a", non dovrebbe mai essere usata per il riutilizzo. Questo è il secondo errore più grande che si possa fare in C ++, ed è piuttosto grande, ma è tutt'altro che unico per la lingua. Puoi creare relazioni ereditarie eccessivamente complesse anche in C # o Java, e avranno esattamente gli stessi problemi.

    
risposta data 07.02.2011 - 22:34
fonte
19

Ho sempre pensato che i pericoli del C ++ fossero altamente esagerati da programmatori C con Classi inesperti.

Sì, C ++ è più difficile da reperire rispetto a qualcosa come Java, ma se si programma usando tecniche moderne è piuttosto facile scrivere programmi robusti. Onestamente non ho che molto più difficile di una programmazione temporale in C ++ di quanto non faccia in linguaggi come Java, e spesso mi trovo a mancare alcune astrazioni C ++ come i modelli e RAII quando disegno in altre lingue .

Detto questo, anche dopo anni di programmazione in C ++, di tanto in tanto farò un errore davvero stupido che non sarebbe possibile in un linguaggio di livello superiore. Una trappola comune in C ++ è ignorare la durata dell'oggetto: in Java e C # in genere non ci si deve preoccupare della durata dell'oggetto *, poiché tutti gli oggetti sono presenti nell'heap e sono gestiti per te da un garbage collector magico.

Ora, nel moderno C ++, di solito non devi preoccuparti troppo della vita dell'oggetto. Hai distruttori e puntatori intelligenti che gestiscono la vita degli oggetti per te. Il 99% delle volte funziona perfettamente. Ma di tanto in tanto, ti viene fregato da un puntatore pendente (o riferimento). Ad esempio, di recente ho avuto un oggetto (chiamiamolo Foo ) che conteneva una variabile di riferimento interna a un altro oggetto (chiamiamola %codice%). A un certo punto, ho sistemato stupidamente le cose in modo che Bar uscisse dall'ambito prima di Bar , eppure il distruttore di Foo ha finito per chiamare una funzione membro di Foo . Inutile dire che le cose non sono andate bene.

Ora, non posso davvero incolpare C ++ per questo. Era il mio cattivo design, ma il punto è che questo genere di cose non accadrebbe in un linguaggio gestito di livello superiore. Anche con puntatori intelligenti e simili, a volte hai ancora bisogno di avere una consapevolezza della vita dell'oggetto.


* Se la risorsa gestita è memoria, cioè.

    
risposta data 07.02.2011 - 22:46
fonte
13

La differenza nel codice è in genere più correlata al programmatore rispetto alla lingua. In particolare, un buon programmatore C ++ e un programmatore C arriveranno a soluzioni altrettanto buone (anche se diverse). Ora, C è un linguaggio più semplice (come lingua) e ciò significa che ci sono meno astrazioni e più visibilità su ciò che effettivamente fa il codice.

Una parte del suo rant (è noto per i suoi rant contro C ++) si basa sul fatto che più persone assumeranno il C ++ e scriveranno codice senza capire veramente cosa nascondono alcune delle astrazioni e fanno assunzioni sbagliate.

    
risposta data 07.02.2011 - 21:35
fonte
12

Uso eccessivo di blocchi try/catch .

File file("some.txt");
try
{
  /**/

  file.close();
}
catch(std::exception const& e)
{
  file.close();
}

Questo di solito deriva da linguaggi come Java e la gente sosterrà che C ++ manca di una clausola finalize .

Ma questo codice presenta due problemi:

  • È necessario creare file prima di try/catch , perché non puoi effettivamente close un file che non esiste in catch . Ciò porta a una "perdita di portata" in quanto file è visibile dopo essere stato chiuso. Puoi aggiungere un blocco ma ...: /
  • Se qualcuno si avvicina e aggiunge un return nel mezzo dell'ambito try , il file non viene chiuso (motivo per cui le persone si lamentano della mancanza della clausola finalize )

Tuttavia, in C ++, abbiamo modi molto più efficienti di affrontare questo problema che:

  • finalize di Java
  • C #% s% co_de
  • Vai using

Abbiamo RAII, la cui proprietà veramente interessante è riassunta come defer (Scoped Bound Resources Management).

Creando la classe in modo che il suo distruttore pulisca le risorse che possiede, non investiamo l'onere di gestire la risorsa su ognuno dei suoi utenti!

Questa è la caratteristica che mi manca in qualsiasi altra lingua, e probabilmente quella che è più dimenticata.

La verità è che raramente c'è bisogno di scrivere anche un blocco SBRM in C ++, a parte il livello più alto per evitare la terminazione senza registrazione.

    
risposta data 10.02.2011 - 19:52
fonte
9

Un errore comune che si adatta ai tuoi criteri non è capire come funzionano i costruttori di copie quando si ha a che fare con la memoria allocata nella tua classe. Ho perso il conto della quantità di tempo che ho impiegato per riparare crash o perdite di memoria perché un "noob" ha inserito i propri oggetti in una mappa o in un vettore e non ha scritto correttamente costruttori e distruttori di copia.

Sfortunatamente il C ++ è pieno di trucchi "nascosti" come questo. Ma lamentarsi è come lamentarsi che sei andato in Francia e non hai capito cosa dicevano le persone. Se hai intenzione di andare lì, impara la lingua.

    
risposta data 07.02.2011 - 22:49
fonte
6

C ++ consente una grande varietà di funzionalità e stili di programmazione, ma ciò non significa che questi siano effettivamente buoni modi per utilizzare C ++. E infatti, è incredibilmente facile usare C ++ in modo errato.

Deve essere appreso e compreso correttamente , solo apprendere facendo (o usarlo come se si usasse un altro linguaggio) porterà a un codice inefficiente e soggetto a errori.

    
risposta data 07.02.2011 - 21:50
fonte
4

Bene ... Per cominciare, puoi leggere Domande frequenti su C ++ Lite

Poi, diverse persone hanno costruito carriere nello scrivere libri sulle complessità del C ++:

Herb Sutter e Scott Meyers vale a dire.

Per quanto riguarda il rant di Torvalds che manca di sostanza ... dai, gente sul serio: nessun'altra lingua là fuori ha avuto così tanto inchiostro rovesciato nel trattare con le sfumature della lingua. Il tuo Python & Ruby & I libri Java si concentrano tutti sulla scrittura di applicazioni ... i tuoi libri C ++ si concentrano su funzionalità / suggerimenti / trappole linguistici stupidi.

    
risposta data 10.02.2011 - 12:18
fonte
3

I modelli troppo pesanti potrebbero non dare origine a bug in un primo momento. Col passare del tempo, tuttavia, le persone avranno bisogno di modificare quel codice, e avranno difficoltà a capire un modello enorme. Ecco quando i bug entrano - le incomprensioni causano commenti "Compilare ed eseguire", che spesso portano a un codice quasi-non-proprio-corretto.

Generalmente, se mi vedo a fare un modello generico di tre livelli, mi fermo e penso a come potrebbe essere ridotto a uno. Spesso il problema si risolve estraendo funzioni o classi.

    
risposta data 07.02.2011 - 21:16
fonte
2

Attenzione: questa non è una risposta tanto quanto una critica del discorso a cui "utente sconosciuto" è stato collegato nella sua risposta.

Il suo primo punto principale è il (presumibilmente) "standard in continua evoluzione". In realtà, gli esempi che fornisce riguardano tutti i cambiamenti in C ++ prima c'era uno standard. Dal 1998 (quando il primo standard C ++ è stato finalizzato) le modifiche al linguaggio sono state abbastanza minime - in realtà, molti sostengono che il vero problema è che dovevano essere apportate modifiche più . Sono ragionevolmente certo che tutto il codice conforme allo standard C ++ originale sia ancora conforme allo standard attuale. Anche se è un po ' meno sicuro, a meno che qualcosa cambi rapidamente (e in modo inaspettato) lo stesso sarà praticamente vero anche con lo standard C ++ in arrivo (teoricamente, tutto il codice che ha usato export si interromperà, ma praticamente nessuno esiste, dal punto di vista pratico non è un problema). Riesco a pensare a pochi altri linguaggi, sistemi operativi (o molto altro collegato al computer) che possono presentare una simile richiesta.

Quindi entra in "stili in continua evoluzione". Ancora una volta, la maggior parte dei suoi punti sono piuttosto vicini alle sciocchezze. Cerca di caratterizzare for (int i=0; i<n;i++) come "old and busted" e for (int i(0); i!=n;++i) "new hotness". La realtà è che mentre ci sono tipi per i quali tali cambiamenti potrebbero avere senso, per int , non fa differenza - e anche quando potresti ottenere qualcosa, è raramente necessario per scrivere codice buono o corretto. Anche nel migliore dei casi, sta facendo una montagna da una talpa.

La sua prossima affermazione è che il C ++ sta "ottimizzando nella direzione sbagliata" - in particolare, mentre ammette che l'uso di buone librerie è facile, sostiene che C ++ "rende quasi impossibile la scrittura di buone librerie". Qui, credo che sia uno dei suoi errori più fondamentali. In realtà, scrivere delle buone librerie per quasi qualsiasi linguaggio è estremamente difficile. Come minimo, scrivere una buona libreria richiede la comprensione di alcuni domini problematici così bene che il tuo codice funziona per una moltitudine di possibili applicazioni in (o relative a) quel dominio. La maggior parte di ciò che C ++ realmente fa è "alzare il livello" - dopo aver visto quanto è migliore una can libreria, le persone raramente sono disposte a tornare a scrivere il tipo di dreck avrebbero altrimenti. Ignora anche il fatto che alcuni veramente buoni codificatori scrivono un bel po 'di librerie, che possono quindi essere usate (facilmente, come ammette) da "il resto di noi". Questo è davvero un caso in cui "non è un bug, è una caratteristica".

Non cercherò di colpire ogni punto in ordine (ciò richiederebbe pagine), ma salterò direttamente al suo punto di chiusura. Egli cita Bjarne nel dire: "l'ottimizzazione dell'intero programma può essere utilizzata per eliminare tabelle di funzioni virtuali e dati RTTI inutilizzati. Tale analisi è particolarmente adatta per programmi relativamente piccoli che non utilizzano collegamenti dinamici."

Egli critica questo affermando che "Questo è un veramente problema difficile", arrivando addirittura a paragonarlo al problema dell'arresto. In realtà, non è nulla del genere - infatti, il linker incluso in Zortech C ++ (praticamente il primo compilatore C ++ per MS-DOS, negli anni '80) lo ha fatto. È vero che è difficile essere certi che tutti i dati potenzialmente estranei siano stati eliminati, ma è comunque del tutto ragionevole fare un lavoro abbastanza equo.

Indipendentemente da ciò, tuttavia, il punto più importante è che questo è assolutamente irrilevante per la maggior parte dei programmatori in ogni caso. Come quelli di noi che hanno smontato un bel po 'di codice sanno, a meno che non si scriva linguaggio assembly senza librerie, i tuoi eseguibili contengono quasi sicuramente una buona quantità di "roba" (sia codice che dati, in casi tipici) che tu probabilmente non ne so nemmeno, per non parlare mai in realtà usando. Per la maggior parte delle persone, il più delle volte, non importa, a meno che non si stia sviluppando per i più piccoli sistemi embedded, che il consumo extra di storage sia semplicemente irrilevante.

Alla fine, è vero che questo sproloquio ha un po 'più di sostanza dell'idiozia di Linus - ma questo gli dà esattamente la schiacciante lode che merita.

    
risposta data 10.02.2011 - 21:21
fonte
1

Come programmatore C che ha dovuto codificare in C ++ a causa di circostanze inevitabili, ecco la mia esperienza. Ci sono pochissime cose che uso che è C ++ e per lo più si attaccano a C. Il motivo principale è perché non capisco molto bene il C ++. Ho fatto / non ho un mentore per mostrarmi le complessità del C ++ e come scrivere un buon codice in esso. E senza la guida di un ottimo codice C ++, è estremamente difficile scrivere un buon codice in C ++. IMHO questo è il più grande svantaggio del C ++ perché è difficile trovare buoni coder C ++ disposti a tenere i principianti.

Alcuni dei colpi di prestazioni che ho visto di solito sono dovuti alla magica allocazione di memoria di STL (sì, puoi cambiare l'allocatore, ma chi lo fa quando inizia con C ++?). Solitamente si ascoltano le argomentazioni degli esperti di C ++ che i vettori e gli array offrono prestazioni simili, poiché i vettori utilizzano gli array internamente e l'astrazione è super efficiente. Ho trovato questo nella pratica per l'accesso vettoriale e la modifica dei valori esistenti. Ma non è vero per aggiungere una nuova voce, costruzione e distruzione di vettori. gprof ha mostrato che cumulativamente il 25% del tempo per un'applicazione è stato speso in costruttori di vettori, distruttori, memmove (per il riposizionamento dell'intero vettore per l'aggiunta di nuovi elementi) e altri operatori vettoriali sovraccaricati (come ++).

Nella stessa applicazione, vettore di qualcosa è stato usato Small per rappresentare qualcosa di grande. Non c'era bisogno di un accesso casuale a qualcosa di piccolo in qualcosa di grande. E 'stato usato un vettore invece di una lista. Il motivo per cui è stato utilizzato il vettore? Perché il coder originale aveva familiarità con la sintassi dei vettori come array e non aveva molta familiarità con gli iteratori necessari per gli elenchi (sì, lui è di origine C). Continua a dimostrare che sono necessari molti consigli dagli esperti per ottenere il C ++ giusto. C offre così pochi costrutti di base senza alcuna astrazione, che puoi ottenere molto più facilmente di C ++.

    
risposta data 08.02.2011 - 04:43
fonte
0

Anche se mi piace Linus Thorvalds, questo sproloquio è senza sostanza - solo un rant.

Se ti piace vedere uno sproloquio provato, eccone uno: "Perché il C ++ è dannoso per l'ambiente, causa il riscaldamento globale e uccide i cuccioli" link Materiale aggiuntivo: link

Un discorso divertente, imho

    
risposta data 08.02.2011 - 09:34
fonte
0

STL e boost sono portatili, a livello di codice sorgente. Credo che di cosa stia parlando Linus è che C ++ manca di un ABI (interfaccia binaria dell'applicazione). Quindi è necessario compilare tutte le librerie con le quali si collega, con la stessa versione del compilatore e con le stesse opzioni, oppure limitarsi al C ABI ai limiti delle DLL. Trovo anche che annyoing .. ma a meno che tu non stia creando librerie di terze parti, dovresti essere in grado di prendere il controllo del tuo ambiente di costruzione. Trovo che limitarmi al C ABI non valga la pena. La comodità di poter passare stringhe, vettori e puntatori intelligenti da una dll a un'altra vale la pena di dover ricostruire tutte le librerie quando si aggiornano i compilatori o si modificano le opzioni del compilatore. Le regole d'oro che seguo sono:

-Eredita di riutilizzare l'interfaccia, non l'implementazione

-Prefering aggregation over inheritance

-Preferire dove possibile funzioni gratuite ai metodi dei membri

- Usa sempre l'idioma RAII per rendere il tuo codice strongmente sicuro. Evita try catch.

-Utilizza puntatori intelligenti, evita puntatori nudi (non proprietari)

-Preferenze semantica del valore per riferimento alla semantica

-Non reinventare la ruota, usare stl e boost

: usa l'idioma Pimpl per nascondere il privato e / o fornire un firewall per il compilatore

    
risposta data 10.02.2011 - 10:51
fonte
-5

Non inserire un ; finale alla fine di una dichiarazione di clase, almeno in alcune versioni di VC.

    
risposta data 07.02.2011 - 22:53
fonte

Leggi altre domande sui tag