it-swarm.it

Peggior pratiche in C ++, errori comuni

Dopo aver letto questo famoso rant di Linus Torvalds , mi chiedevo quali fossero in realtà tutte le insidie ​​per i programmatori in C++. Non mi riferisco esplicitamente a errori di battitura o flusso di programma errato come trattato in questa domanda e le sue risposte , ma a errori di più alto livello che non vengono rilevati dal compilatore e non provocano errori evidenti a prima esecuzione, errori di progettazione completi, cose che sono improbabili in C ma che probabilmente verranno eseguite in C++ da nuovi arrivati ​​che non comprendono le implicazioni complete del loro codice.

Accolgo con favore anche le risposte sottolineando un enorme calo delle prestazioni dove normalmente non ci si aspetterebbe. Un esempio di ciò che uno dei miei professori una volta mi disse di un generatore di parser LR (1) che scrissi:

Hai usato un numero eccessivo di casi di ereditarietà e virtualità non necessari. L'ereditarietà rende un progetto molto più complicato (e inefficiente a causa del sottosistema RTTI (inferenza di tipo run-time)) e pertanto deve essere utilizzato solo dove ha senso, ad es. per le azioni nella tabella di analisi. Poiché fai un uso intensivo dei modelli, praticamente non hai bisogno dell'ereditarietà ".

35
Felix Dombek

Torvalds sta parlando dal suo culo qui.


OK, perché sta parlando dal suo culo:

Prima di tutto, il suo sfogo è davvero niente MA irruzione. C'è pochissimo contenuto reale qui. L'unico motivo per cui è davvero famoso o anche leggermente rispettato è perché è stato creato dal dio Linux. Il suo argomento principale è che il C++ è una schifezza e gli piace far incazzare le persone in C++. Ovviamente non c'è motivo di rispondere a questo e chiunque lo consideri un argomento ragionevole è comunque fuori discussione.

Quanto a ciò che potrebbe essere brillato come i suoi punti più oggettivi:

  • STL e Boost sono una vera merda <- qualunque cosa. Sei un idiota.
  • STL e Boost causano infinite quantità di dolore <- ridicolo. Ovviamente sta volutamente esagerando, ma qual è la sua vera affermazione qui? Non lo so. Ci sono alcuni problemi più che banalmente difficili da capire quando si causa il vomito del compilatore in Spirit o qualcosa del genere, ma non è più o meno difficile da capire rispetto al debug di UB causato dall'abuso di costrutti C come vuoto *.
  • I modelli astratti incoraggiati dal C++ sono inefficienti. <- Ti piace cosa? Non si espande mai, non fornisce mai esempi di ciò che intende, lo dice semplicemente. BFD. Dal momento che non posso dire a cosa si riferisca, è inutile cercare di "confutare" l'affermazione. È un mantra comune di C bigotti ma ciò non lo rende più comprensibile o intelligibile.
  • L'uso corretto di C++ significa limitarsi agli aspetti C. <- In realtà il codice WORSE C++ disponibile lo fa, quindi non so ancora di che WTF stia parlando.

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

Solo perché Dio dice che non significa che abbia alcun senso o che dovrebbe essere preso più seriamente che se un bozo casuale lo dicesse. A dire il vero, Dio è solo un altro bozo casuale.


Rispondere alla domanda effettiva:

Probabilmente la peggiore e più comune, cattiva pratica C++ è trattarla come C. L'uso continuato delle funzioni dell'API C come printf, diventa (anche considerato cattivo in C), strtok, ecc ... non solo non riesce a sfruttare la potenza fornita dal sistema di tipo più stretto, portano inevitabilmente a ulteriori complicazioni quando si cerca di interagire con il codice C++ "reale". Quindi, in pratica, fai esattamente l'opposto di ciò che Torvalds sta suggerendo.

Impara a sfruttare STL e Boost per ottenere un ulteriore rilevamento del tempo di compilazione dei bug e per semplificarti la vita in altri modi generali (il tokenizer boost, ad esempio, è sia sicuro da scrivere che un'interfaccia migliore). È vero che dovrai imparare a leggere gli errori del modello, che inizialmente sono scoraggianti, 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, che l'API fa abbastanza facile da fare.

Per non dire che C non è buono. Ovviamente mi piace il C++ meglio. Ai programmatori C piace C meglio. Ci sono compromessi e Mi piace soggettivi in ​​gioco. C'è anche molta disinformazione e FUD in giro. Direi che ci sono più FUD e disinformazione in circolazione su C++, ma sono di parte in questo senso. Ad esempio, i problemi di "gonfiore" e "prestazioni" presumibilmente il C++ non sono in realtà problemi importanti 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 esclusivi del C++. In OOP (e nella programmazione generica) si desidera preferire la composizione rispetto all'ereditarietà. L'ereditarietà è la relazione di accoppiamento più forte possibile che esiste in tutti i linguaggi OO. C++ aggiunge un altro che è più forte, 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 puoi fare in C++, ed è piuttosto grande , ma è tutt'altro che univoco per la lingua. Puoi creare relazioni ereditarie troppo complesse in C # o Java, e avranno esattamente gli stessi problemi.

69
Edward Strange

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

Sì, C++ è più difficile da imparare rispetto a qualcosa come Java, ma se programmi usando tecniche moderne è abbastanza facile scrivere programmi robusti. Onestamente non ho quello molto più difficile di una programmazione temporale in C++ di quanto non faccia in linguaggi come Java, e spesso mi ritrovo a mancare certe astrazioni C++ come template e RAII quando disegno in altri linguaggi .

Detto questo, anche dopo anni di programmazione in C++, ogni tanto commetterò un errore davvero stupido che non sarebbe possibile in un linguaggio di livello superiore. Una trappola comune in C++ è ignorare la durata degli oggetti: in Java e C # generalmente non devi preoccuparti della durata degli oggetti *, perché tutti gli oggetti esistono nell'heap e sono gestiti per te da un magico bidone della spazzatura.

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

Ora, non posso davvero dare la colpa a 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 è ancora necessario avere una consapevolezza della vita degli oggetti.


* Se la risorsa gestita è la memoria, cioè.

19
Charles Salvia

Uso eccessivo di try/catch blocchi.

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 al C++ manca 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 ambito" in quanto file è visibile dopo essere stato chiuso. Puoi aggiungere un blocco ma ...: /
  • Se qualcuno viene in giro 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:

  • Java finalize
  • C # 's using
  • Vai a defer

Abbiamo RAII, la cui proprietà davvero interessante è meglio sintetizzata come SBRM (Gestione delle risorse vincolate al campo di applicazione).

Creando la classe in modo che il suo distruttore ripulisca le risorse che possiede, non ci impegniamo a 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 è necessario persino scrivere un try/catch blocco in C++, a parte al livello più alto per evitare la chiusura senza registrazione.

13
Matthieu M.

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

Una parte del suo sfogo (è noto per i suoi risentimenti contro il C++) si basa sul fatto che più persone prenderanno il C++ e scriveranno il codice senza realmente capire ciò che alcune astrazioni nascondono e fanno ipotesi sbagliate.

Un errore comune che si adatta ai tuoi criteri non è capire come funzionano i costruttori di copie quando si tratta di memoria allocata nella tua classe. Ho perso il conto del tempo che ho trascorso a riparare crash o perdite di memoria perché un "noob" ha messo i loro oggetti in una mappa o in un vettore e non ha scritto correttamente i costruttori di copie e i distruttori.

Sfortunatamente il C++ è pieno di gotcha "nascosti" come questo. Ma lamentarti è come lamentarti del fatto che sei andato in Francia e non hai capito cosa dicevano le persone. Se andrai lì, impara la lingua.

9
Henry

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

Deve essere appreso e compreso correttamente , l'apprendimento facendo (o usandolo come si userebbe un'altra lingua) porterà a un codice inefficace e soggetto a errori.

6
Dario

Bene ... Per cominciare puoi leggere C++ FAQ Lite

Quindi, diverse persone hanno costruito carriere scrivendo 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, sul serio: nessun altra lingua là fuori ha versato così tanto inchiostro nel trattare le sfumature della lingua. I tuoi Python & Ruby & Java i libri sono tutti incentrati sulla scrittura di applicazioni ... i tuoi libri C++ si concentrano su funzionalità del linguaggio sciocco/suggerimenti/trappole.

4
red-dirt

Un templating eccessivo potrebbe non causare inizialmente errori. Col passare del tempo, tuttavia, le persone dovranno modificare quel codice e avranno difficoltà a comprendere un modello enorme. Questo è quando i bug entrano: l'incomprensione causa commenti "Compila ed esegue", che spesso portano a un codice quasi ma non del tutto corretto.

In genere, se mi vedo fare un modello generico profondo a tre livelli, mi fermo e penso a come ridurlo a uno. Spesso il problema viene risolto estraendo funzioni o classi.

3
Michael K

Avvertenza: questa non è una risposta tanto quanto una critica alla conversazione a cui "l'utente sconosciuto" ha collegato la sua risposta.

Il suo primo punto principale è il (presumibilmente) "standard in continua evoluzione". In realtà, gli esempi che fornisce sono tutti relativi a cambiamenti in C++ prima che esistesse uno standard. Dal 1998 (quando è stato finalizzato il primo standard C++) le modifiche al linguaggio sono state abbastanza minime - in effetti, molti sostengono che il vero problema è che più avrebbero dovuto essere apportate modifiche. Sono ragionevolmente certo che tutto il codice conforme allo standard C++ originale sia ancora conforme allo standard attuale. Sebbene sia un po ' meno sicuro, a meno che qualcosa cambi rapidamente (e abbastanza inaspettatamente) lo stesso sarà praticamente vero anche con il prossimo standard C++ (teoricamente, tutto il codice che utilizzava export si romperà, ma praticamente non esiste; dal punto di vista pratico non è un problema). Mi vengono in mente poche altre lingue, sistemi operativi (o gran parte di qualsiasi altra cosa relativa al computer) in grado di presentare una simile richiesta.

Quindi passa a "stili in continua evoluzione". Ancora una volta, la maggior parte dei suoi punti sono abbastanza 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 alcuna differenza - e anche quando potresti ottenere qualcosa, è raramente necessario per scrivere codice buono o corretto. Anche nella migliore delle ipotesi, sta costruendo una montagna da una talpa.

La sua prossima affermazione è che C++ sta "ottimizzando nella direzione sbagliata" - in particolare, mentre ammette che usare buone librerie è facile, afferma che C++ "rende quasi impossibile scrivere buone librerie". Qui, credo sia uno dei suoi errori più fondamentali. In realtà, scrivere buone librerie per quasi qualsiasi lingua è estremamente difficile. Come minimo, per scrivere una buona libreria è necessario comprendere un dominio del problema così bene che il codice funziona per una moltitudine di possibili applicazioni in (o relative a) quel dominio. La maggior parte di ciò che fa C++ è "alzare il livello" - dopo aver visto quanto meglio una libreria può essere, le persone raramente sono disposte a tornare a scrivere il tipo di dreck che avrebbero altrimenti. Ignora anche il fatto che alcuni veramente buoni programmatori scrivono parecchie librerie, che possono quindi essere usate (facilmente, come ammette) con "il il resto di noi". Questo è davvero un caso in cui "non è un bug, è una funzionalità".

Non proverò a centrare tutti i punti in ordine (ciò richiederebbe delle pagine), ma salterò direttamente al suo punto di chiusura. Cita Bjarne dicendo: "L'ottimizzazione dell'intero programma può essere utilizzata per eliminare tabelle di funzioni virtuali inutilizzate e dati RTTI. Tale analisi è particolarmente adatta per programmi relativamente piccoli che non utilizzano il collegamento dinamico".

Egli critica questo affermando che "Questo è un davvero problema difficile", persino spingendolo fino a paragonarlo al problema di arresto. In realtà, non è niente del genere - in effetti, il linker incluso con Zortech C++ (praticamente il primo compilatore C++ per MS-DOS, indietro negli anni '80) ha fatto questo. È vero che è difficile essere certi che tutti i dati eventualmente estranei siano stati eliminati, ma è comunque del tutto ragionevole fare un lavoro abbastanza giusto.

Indipendentemente da ciò, tuttavia, il punto molto più importante è che questo è assolutamente irrilevante per la maggior parte dei programmatori in ogni caso. Come sanno quelli di noi che hanno disassemblato un po 'di codice, a meno che non si scriva un linguaggio Assembly senza librerie, i file eseguibili contengono quasi sicuramente una discreta quantità di "elementi" (sia codice che dati, in casi tipici) che si probabilmente non ne sono nemmeno a conoscenza, per non parlare del fatto che in realtà venga utilizzato. Per la maggior parte delle persone, il più delle volte, non importa, a meno che non si stia sviluppando per i sistemi embedded più piccoli, il consumo aggiuntivo di spazio di archiviazione sia semplicemente irrilevante.

Alla fine, è vero che questo sfogo ha un po 'più di sostanza dell'idiozia di Linus - ma questo gli dà esattamente il danno con deboli elogi che merita.

2
Jerry Coffin

Come programmatore C che ha dovuto programmare in C++ a causa di circostanze inevitabili, ecco la mia esperienza. Ci sono pochissime cose che uso in C++ e per lo più attenersi a C. Il motivo principale è perché non capisco bene C++. Avevo/non avevo 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 programmatori C++ disposti a gestire i principianti.

Alcuni dei successi 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++?). Di solito senti gli argomenti degli esperti C++ che i vettori e gli array offrono prestazioni simili, perché i vettori usano gli array internamente e l'astrazione è super efficiente. Ho scoperto che ciò è vero nella pratica per l'accesso ai vettori e la modifica dei valori esistenti. Ma non è vero per l'aggiunta di una nuova voce, costruzione e distruzione di vettori. gprof ha mostrato che cumulativamente il 25% del tempo per un'applicazione è stato impiegato in costruttori di vettori, distruttori, memmove (per il trasferimento di interi vettori per l'aggiunta di nuovi elementi) e altri operatori vettoriali sovraccarichi (come ++).

Nella stessa applicazione, il vettore di qualcosa di piccolo è stato usato per rappresentare qualcosa di grosso. Non c'era bisogno di un accesso casuale a qualcosa di piccolo in qualcosa di grosso. È stato comunque utilizzato un vettore anziché un elenco. Il motivo per cui è stato utilizzato il vettore? Perché il programmatore originale aveva familiarità con la sintassi dei vettori come l'array e non aveva molta familiarità con gli iteratori necessari per gli elenchi (sì, viene da uno sfondo C). Continua a dimostrare che sono necessarie molte indicazioni da parte di esperti per ottenere il C++ giusto. C offre così pochi costrutti di base senza alcuna astrazione, che puoi ottenerlo molto più facilmente di C++.

1
aufather

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

Se ti piace vedere una prova sostanziale, eccone una: "Perché il C++ fa male all'ambiente, causa il riscaldamento globale e uccide i cuccioli" http://chaosradio.ccc.de/camp2007_m4v_1951.html Ulteriori materiale: http://www.fefe.de/c++/

Un discorso divertente, imho

0
user unknown

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

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

-Preferire l'aggregazione rispetto all'eredità

-Preferire ove possibile funzioni gratuite ai metodi membro

-Usare sempre il linguaggio RAII per rendere il codice fortemente sicuro. Evita di provare a catturare.

-Utilizzare puntatori intelligenti, evitare puntatori nudi (non di proprietà)

-Preferire la semantica del valore per fare riferimento alla semantica

-Non reinventare la ruota, usare stl e boost

-Utilizzare il linguaggio Pimpl per nascondere privato e/o per fornire un firewall del compilatore

0
user16642