Fa sì che il codice C ++ venga reso più difficile da capire?

119

Ho visto una conferenza di Herb Sutter in cui incoraggia ogni programmatore C ++ a usare auto .

Ho dovuto leggere il codice C # qualche tempo fa in cui var era ampiamente utilizzato e il codice era molto difficile da capire-ogni volta che veniva usato var dovevo controllare il tipo di ritorno del lato destro. A volte più di una volta, perché ho dimenticato il tipo della variabile dopo un po '!

So che il compilatore conosce il tipo e non devo scriverlo, ma è ampiamente accettato che dovremmo scrivere codice per i programmatori, non per i compilatori.

So anche che è più facile scrivere:

auto x = GetX();

Than:

someWeirdTemplate<someOtherVeryLongNameType, ...>::someOtherLongType x = GetX();

Ma questo è scritto solo una volta e il tipo di ritorno GetX() è spuntato molte volte per capire che tipo x ha.

Questo mi ha fatto meravigliare: il auto rende più difficile comprendere il codice C ++?

    
posta Felics 20.12.2012 - 15:01
fonte

14 risposte

92

Risposta breve: più completamente, la mia opinione corrente su auto è che dovresti usare auto per impostazione predefinita, a meno che tu non desideri esplicitamente una conversione. (Un po 'più precisamente, "... a meno che non si desideri impegnarsi esplicitamente in un tipo, che è quasi sempre perché si desidera una conversione.")

Risposta e motivazione più lunghe:

Scrivi un tipo esplicito (anziché auto ) solo quando vuoi veramente impegnarti esplicitamente in un tipo, il che significa quasi sempre che vuoi ottenere esplicitamente una conversione per quel tipo. In cima alla mia testa, ricordo due casi principali:

  • (Comune) La sorpresa di initializer_list che auto x = { 1 }; deduce initializer_list . Se non vuoi initializer_list , dì il tipo - ad esempio, chiedi esplicitamente una conversione.
  • (Raro) Il caso dei modelli di espressione, ad esempio auto x = matrix1 * matrix 2 + matrix3; , cattura un tipo di helper o proxy che non deve essere visibile al programmatore. In molti casi, è bello e benigno catturare quel tipo, ma a volte se vuoi davvero che crolli e compia il calcolo, allora pronuncia il tipo - ad esempio, di nuovo chiedi esplicitamente una conversione.

In genere usa auto di default altrimenti, poiché l'utilizzo di auto evita le insidie e rende il tuo codice più corretto, più gestibile, robusto e più efficiente. Circa in ordine dalla maggior parte al meno importante, nello spirito di "scrivere per chiarezza e correttezza prima":

  • Correttezza: l'utilizzo di auto garantisce che otterrai il tipo giusto. Come dice il proverbio, se ti ripeti (diciamo il tipo in modo ridondante), puoi e mentirai (sbaglia). Ecco un esempio normale: void f( const vector<int>& v ) { for( /*…* - a questo punto, se scrivi esplicitamente il tipo dell'iteratore, vuoi ricordare di scrivere const_iterator (hai fatto?), Mentre auto lo fa proprio bene.
  • Manutenibilità e robustezza: l'utilizzo di auto rende il codice più solido di fronte alle modifiche, perché quando cambia il tipo dell'espressione, auto continuerà a risolversi nel tipo corretto. Se invece si esegue il commit su un tipo esplicito, la modifica del tipo dell'espressione inserisce le conversioni invisibili quando il nuovo tipo viene convertito al vecchio tipo o interruzioni di generazione inutili quando il nuovo tipo funziona ancora, come il vecchio tipo ma non viene convertito nel vecchio type (ad esempio, quando cambi una map in una unordered_map , che va sempre bene se non ti stai affidando all'ordine, usando auto per i tuoi iteratori passerai senza problemi da map<>::iterator a unordered_map<>::iterator , ma usare map<>::iterator ovunque significa esplicitamente che stai sprecando il tuo tempo prezioso su un'increspatura di un fix di codice meccanico, a meno che uno stagista stia camminando e tu possa scaricare il lavoro noioso su di loro).
  • Rendimento: poiché auto garantisce che non avvenga alcuna conversione implicita, garantisce prestazioni migliori per impostazione predefinita. Se invece dici il tipo e richiede una conversione, riceverai spesso una conversione silenziosa, indipendentemente dal fatto che te l'aspetti o meno.
  • Usabilità: l'utilizzo di auto è la tua unica buona opzione per i tipi difficili da incantare e imprecisabili, come lambdas e template helper, a meno di ricorrere alle espressioni ripetitive decltype o alle indecisioni meno efficienti come std::function .
  • Convenienza: e, sì, auto è meno digitante. Ne parlo per ultimo per completezza perché è un motivo comune per piacermi, ma non è il motivo principale per usarlo.

Quindi: preferisci dire auto per impostazione predefinita. Offre così tanta semplicità e bontà di prestazioni e chiarezza che stai solo facendo del male a te stesso (e ai futuri manutentori del tuo codice) se non lo fai. Impegni solo su un tipo esplicito quando lo intendi veramente, il che significa quasi sempre che desideri una conversione esplicita.

Sì, c'è (ora) un GotW su questo.

    
risposta data 25.12.2012 - 21:06
fonte
111

È una situazione caso per caso.

A volte rende il codice più difficile da capire, a volte no. Prendi, ad esempio:

void foo(const std::map<int, std::string>& x)
{
   for ( auto it = x.begin() ; it != x.end() ; it++ )
   { 
       //....
   }
}

è sicuramente facile capire e sicuramente più semplice da scrivere rispetto alla dichiarazione effettiva dell'iteratore.

Ho usato C ++ per un po 'di tempo, ma posso garantire che otterrò un errore del compilatore al mio primo tentativo perché mi dimenticherei del const_iterator e inizialmente andrei per iterator ...:)

Lo userei per casi come questo, ma non dove effettivamente offusca il tipo (come la tua situazione), ma questo è puramente soggettivo.

    
risposta data 20.12.2012 - 15:03
fonte
94

Guardalo in un altro modo. Scrivi:

std::cout << (foo() + bar()) << "\n";

o

// it is important to know the types of these values
int f = foo();
size_t b = bar();
size_t total = f + b;

std::cout << total << "\n";

A volte non aiuta a scrivere esplicitamente il tipo in modo esplicito.

La decisione se è necessario menzionare il tipo non è la stessa della decisione se si desidera dividere il codice tra più istruzioni definendo le variabili intermedie. In C ++ 03 i due erano collegati, puoi pensare a auto come modo per separarli.

A volte rendere espliciti i tipi può essere utile:

// seems legit    
if (foo() < bar()) { ... }

vs.

// ah, there's something tricky going on here, a mixed comparison
if ((unsigned int)foo() < bar()) { ... }

Nei casi in cui dichiari una variabile, utilizzando auto il tipo diventa non detto proprio come è in molte espressioni. Probabilmente dovresti provare a decidere da solo quando ciò aiuta la leggibilità e quando ostacola.

Si può sostenere che il mixaggio di tipi firmati e non firmati è un errore da iniziare (anzi, alcuni sostengono ulteriormente che non si dovrebbero usare affatto tipi non firmati). Il motivo è probabilmente un errore è che rende i tipi di operandi di vitale importanza a causa del diverso comportamento. Se è una brutta cosa sapere i tipi di valori, allora probabilmente non è una cattiva idea non aver bisogno di conoscerli. Quindi, se il codice non è già confuso per altri motivi, ciò rende auto OK, giusto? ; -)

Soprattutto quando si scrive codice generico ci sono casi in cui il tipo effettivo di una variabile non dovrebbe essere importante, ciò che importa è che soddisfi l'interfaccia richiesta. Quindi auto fornisce un livello di astrazione in cui si ignora il tipo (ma ovviamente il compilatore no, lo sa). Lavorare ad un livello adeguato di astrazione può aiutare molto la leggibilità, lavorare a livello "sbagliato" rende la lettura del codice uno slog.

    
risposta data 20.12.2012 - 15:08
fonte
26

IMO, stai guardando questo piuttosto al contrario.

Non è questione di auto che porti a un codice illeggibile o persino meno leggibile. È una questione di (sperando che) avere un tipo esplicito per il valore di ritorno compenserà il fatto che è (apparentemente) non chiaro quale tipo verrebbe restituito da qualche funzione particolare.

Almeno secondo me, se hai una funzione il cui tipo di ritorno non è immediatamente evidente, questo è il tuo problema proprio lì. Quale dovrebbe essere la funzione dovrebbe essere ovvia dal suo nome e il tipo del valore restituito dovrebbe essere ovvio da quello che fa. In caso contrario, questa è la vera fonte del problema.

Se c'è un problema qui, non è con auto . È con il resto del codice, e ci sono buone probabilità che il tipo esplicito sia abbastanza di un cerotto per impedirti di vedere e / o correggere il problema principale. Una volta risolto il problema reale, la leggibilità del codice utilizzando auto generalmente andrà bene.

Suppongo che, in tutta onestà, dovrei aggiungere: mi sono occupato di alcuni casi in cui cose del genere non erano così ovvie come vorresti, e anche la risoluzione del problema era abbastanza insostenibile. Solo per un esempio, ho fatto qualche consulenza per una società un paio di anni fa che si era precedentemente fusa con un'altra azienda. Hanno finito con un codice base che era più "spinto insieme" rispetto a un vero insieme. I programmi costitutivi avevano iniziato a utilizzare librerie diverse (ma abbastanza simili) per scopi simili e, sebbene stessero lavorando per unire le cose in modo più pulito, lo facevano ancora. In un discreto numero di casi, l'unico modo per indovinare quale tipo sarebbe stato restituito da una determinata funzione era sapere da dove proveniva quella funzione.

Anche in questo caso, puoi contribuire a rendere più chiare alcune cose. In tal caso, tutto il codice è iniziato nel namespace globale. Il semplice spostamento di una certa quantità in alcuni spazi dei nomi ha eliminato le interferenze dei nomi e ha facilitato anche il tracciamento dei tipi.

    
risposta data 21.12.2012 - 06:51
fonte
14

Sì, rende più facile conoscere il tipo della variabile se non si utilizza auto . La domanda è: hai bisogno di sapere il tipo della tua variabile per leggere il codice? A volte la risposta sarà sì, a volte no. Ad esempio, quando ottieni un iteratore da std::vector<int> , devi sapere che è un std::vector<int>::iterator o auto iterator = ...; sufficiente? Tutto ciò che qualcuno vorrebbe fare con un iteratore è dato dal fatto che si tratta di un iteratore: non importa quale sia il tipo in particolare.

Utilizza auto in quelle situazioni in cui il codice non è più difficile da leggere.

    
risposta data 20.12.2012 - 15:05
fonte
14

Ci sono diversi motivi per cui non mi piace l'auto per uso generale:

  1. Puoi rifattorizzare il codice senza modificarlo. Sì, questa è una delle cose spesso elencate come un vantaggio dell'uso dell'auto. Basta cambiare il tipo di ritorno di una funzione, e se tutto il codice che la chiama usa auto, non è richiesto alcuno sforzo aggiuntivo! Ottieni la compilazione, genera - 0 avvisi, 0 errori - e vai avanti e controlla il tuo codice senza dover affrontare il caos di guardare attraverso e potenzialmente modificare gli 80 luoghi in cui viene utilizzata la funzione.

Ma aspetta, è davvero una buona idea? Cosa accadrebbe se il tipo fosse importante in una mezza dozzina di quei casi d'uso, e ora che il codice si comporta effettivamente in modo diverso? Questo può anche interrompere implicitamente l'incapsulamento, modificando non solo i valori di input, ma il comportamento stesso dell'implementazione privata di altre classi che chiamano la funzione.

1 bis. Sono un credente nel concetto di "codice di auto-documentazione". Il ragionamento dietro il codice di auto-documentazione è che i commenti tendono a diventare obsoleti, non riflettendo più ciò che il codice sta facendo, mentre il codice stesso - se scritto in modo esplicito - è autoesplicativo, rimane sempre aggiornato nel suo intento, e non ti lascerà confuso con commenti stantii. Se i tipi possono essere modificati senza la necessità di modificare il codice stesso, allora il codice / variabili stesse può diventare obsoleto. Ad esempio:

auto bThreadOK = CheckThreadHealth ();

Tranne che il problema è che CheckThreadHealth () a un certo punto è stato refactored per restituire un valore enum che indica lo stato di errore, se presente, invece di un bool. Ma la persona che ha apportato tale modifica ha mancato l'ispezione di questa particolare riga di codice e il compilatore non ha aiutato perché è stato compilato senza avvisi o errori.

  1. Potresti non sapere mai quali sono i tipi effettivi. Questo è anche spesso elencato come un "vantaggio" primario di auto. Perché imparare ciò che una funzione ti sta dando, quando puoi semplicemente dire: "A chi importa?

È anche un po 'come funziona, probabilmente. Dico un po 'di lavoro, perché anche se stai facendo una copia di una struttura di 500 byte per ogni iterazione del ciclo, così puoi controllare un singolo valore su di esso, il codice è ancora completamente funzionale. Quindi anche i tuoi test unitari non ti aiutano a capire che il cattivo codice si nasconde dietro quella auto semplice e dall'aria innocente. La maggior parte delle altre persone che scansionano il file non la noteranno nemmeno a prima vista.

Questo può anche peggiorare se non si conosce quale sia il tipo, ma si sceglie un nome di variabile che fa un'ipotesi errata su quello che è, in effetti ottenendo lo stesso risultato di 1a, ma proprio dal inizio piuttosto che post refactor.

  1. La digitazione del codice quando si scrive inizialmente non è la parte che richiede più tempo della programmazione. Sì, l'auto rende inizialmente la scrittura di un codice più veloce. Come disclaimer, digito > 100 WPM, quindi forse non mi infastidisce tanto quanto gli altri. Ma se tutto quello che dovevo fare era scrivere un nuovo codice tutto il giorno, sarei un campeggiatore felice. La parte più lunga che richiede tempo di programmazione è la diagnosi di errori nel codice, difficili da riprodurre, spesso derivati da sottili problemi non ovvi, come il probabile uso eccessivo di auto (riferimento e copia, firmato vs unsigned, float vs. int, bool vs. pointer, ecc.).

Mi sembra ovvio che l'auto sia stata introdotta principalmente come soluzione alternativa per una sintassi terribile con i tipi di modelli di libreria standard. Piuttosto che cercare di correggere la sintassi del modello con cui le persone hanno già familiarità - il che potrebbe anche essere quasi impossibile da fare a causa dell'intero codice esistente che potrebbe interrompersi - aggiungere una parola chiave che fondamentalmente nasconde il problema. Essenzialmente quello che potreste definire un "hack".

In realtà non ho alcun disaccordo con l'uso dell'auto con i contenitori di libreria standard. È ovvio per che cosa è stata creata la parola chiave, e le funzioni nella libreria standard non sono suscettibili di cambiare radicalmente lo scopo (o il tipo per quella materia), rendendo l'uso relativamente sicuro. Ma sarei molto cauto nell'usarlo con il tuo codice e le tue interfacce che potrebbero essere molto più volatili e potenzialmente soggetti a cambiamenti più fondamentali.

Un'altra utile applicazione di auto che migliora la capacità del linguaggio è la creazione di provini nei macro agnostici del tipo. Questo è qualcosa che non potevi fare prima, ma puoi farlo ora.

    
risposta data 25.07.2015 - 03:09
fonte
12

Personalmente uso auto solo quando è assolutamente ovvio per il programmatore di cosa si tratta.

Esempio 1

std::map <KeyClass, ValueClass> m;
// ...
auto I = m.find (something); // OK, find returns an iterator, everyone knows that

Esempio 2

MyClass myObj;
auto ret = myObj.FindRecord (something)// NOT OK, everyone needs to go and check what FindRecord returns
    
risposta data 20.12.2012 - 15:09
fonte
10

Questa domanda sollecita l'opinione, che varierà da programmatore a programmatore, ma direi di no. In molti casi, al contrario, auto può aiutare a rendere il codice più facile da capire, consentendo al programmatore di concentrarsi sulla logica piuttosto che sulle minuzie.

Questo è particolarmente vero di fronte a tipi di modelli complessi. Ecco una versione semplificata di & esempio forzato. Quale è più facile da capire?

for( std::map<std::pair<Foo,Bar>, std::pair<Baz, Bot>, std::less<BazBot>>::const_iterator it = things_.begin(); it != things_.end(); ++it )

.. o ...

for( auto it = things_.begin(); it != things_.end(); ++it )

Alcuni direbbero che il secondo è più facile da capire, altri potrebbero dire il primo. Eppure altri potrebbero dire che un uso gratuito di auto può contribuire a una sciocchezza dei programmatori che lo usano, ma questa è un'altra storia.

    
risposta data 20.12.2012 - 15:06
fonte
8

Molte buone risposte finora, ma per concentrarmi sulla domanda originale, penso che Herb si spinga troppo oltre nel suo consiglio di usare auto liberamente. Il tuo esempio è un caso in cui l'utilizzo di auto ovviamente danneggia la leggibilità. Alcuni sostengono che si tratti di un problema con IDE moderni in cui è possibile passare con il mouse su una variabile e vedere il tipo, ma non sono d'accordo: talvolta le persone che usano sempre un IDE a volte devono guardare frammenti di codice isolati (si pensi alle revisioni del codice , per esempio) e un IDE non aiuterà.

Bottom line: usa auto quando aiuta: cioè iteratori nei cicli for. Non usarlo quando fa fatica il lettore a scoprire il tipo.

    
risposta data 21.12.2012 - 14:52
fonte
5

Sono abbastanza sorpreso dal fatto che nessuno abbia sottolineato che l'auto aiuta se non c'è un tipo chiaro. In questo caso, puoi ovviare a questo problema utilizzando un #define o un typedef in un modello per trovare il tipo effettivo utilizzabile (e talvolta non è banale), o semplicemente usi auto.

Supponiamo che tu abbia una funzione, che restituisca qualcosa con un tipo specifico della piattaforma:

#ifdef PLATFROM1
__int256 getStuff();
#else //PLATFORM2
__int128 getStuff();
#endif

L'uso della strega preferiresti?

#ifdef PLATFORM1
__int256 stuff = getStuff();
#else
__int128 stuff = getStuff();
#endif

o semplicemente

auto stuff = getStuff();

Certo, puoi scrivere

#define StuffType (...)

pure da qualche parte, ma

StuffType stuff = getStuff();

effettivamente dire qualcosa di più sul tipo di x? Dice che è ciò che viene restituito da lì, ma è esattamente ciò che è l'auto. Questo è ridondante - "stuff" è scritto 3 volte qui - questo a mio parere lo rende meno leggibile rispetto alla versione "auto".

    
risposta data 27.12.2012 - 12:38
fonte
3

La leggibilità è soggettiva; avrai bisogno di guardare la situazione e decidere cosa è meglio.

Come hai sottolineato, senza auto, le dichiarazioni lunghe possono produrre un sacco di confusione. Ma come hai anche sottolineato, le brevi dichiarazioni possono rimuovere informazioni di tipo che possono essere preziose.

Inoltre, aggiungo anche questo: assicurati di considerare la leggibilità e non la scrittura. Il codice facile da scrivere non è generalmente facile da leggere e viceversa. Ad esempio, se scrivessi, preferirei l'auto. Se stavo leggendo, forse le dichiarazioni più lunghe.

Poi c'è coerenza; quanto è importante per te? Vorresti l'auto in alcune parti e le dichiarazioni esplicite in altre o un metodo coerente in tutto?

    
risposta data 20.12.2012 - 15:02
fonte
2

Prenderò in considerazione il punto di codice meno leggibile e incoraggerò il programmatore a usarlo sempre di più. Perché? Chiaramente se il codice che utilizza auto è difficile da leggere, allora sarà difficile anche scrivere. Il programmatore è costretto a usare il nome della variabile significativa , per migliorare il proprio lavoro.
Forse all'inizio il programmatore potrebbe non scrivere i nomi delle variabili significative. Ma alla fine mentre risolvono i bug, o nella revisione del codice, quando lui / lei deve spiegare il codice agli altri, o in un futuro non molto vicino, lui / lei spiegando il codice alle persone di manutenzione, il programmatore realizzerà l'errore e userà il nome della variabile significativa in futuro.

    
risposta data 21.12.2012 - 07:44
fonte
2

Ho due linee guida:

  • Se il tipo di variabile è ovvio, noioso da scrivere o difficile da scrivere determinare l'uso automatico.

    auto range = 10.0f; // Obvious
    
    for (auto i = collection.cbegin(); i != cbegin(); ++i) // Tedious if collection type
    // is really long
    
    template <typename T> ... T t; auto result = t.get(); // Hard to determine as get()
    // might return various stuff
    
  • Se hai bisogno di una conversione specifica o il tipo di risultato non è ovvio e potrebbe causare confusione.

    class B : A {}; A* foo = new B(); // 'Convert'
    
    class Factory { public: int foo(); float bar(); }; int f = foo(); // Not obvious
    
risposta data 15.01.2013 - 10:33
fonte
1

Sì. Riduce la verbosità, ma il malinteso comune è che la verbosità diminuisce la leggibilità. Questo è vero solo se consideri la leggibilità come estetica piuttosto che la tua effettiva capacità di interpretare il codice - che non viene aumentata usando l'auto. Nell'esempio più comunemente citato, vettore iteratore, può apparire sulla superficie che l'uso dell'auto aumenta la leggibilità del codice. D'altra parte, non sai sempre cosa ti darà la parola chiave auto. Devi seguire lo stesso percorso logico che fa il compilatore per fare quella ricostruzione interna, e un sacco di tempo, in particolare con gli iteratori, farai le assunzioni sbagliate.

Alla fine della giornata "auto" sacrifica la leggibilità del codice e della chiarezza, per la "pulizia" sintattica ed estetica (che è necessaria solo perché gli iteratori hanno una sintassi inutilmente complicata) e la possibilità di digitare 10 caratteri in meno su ogni dato linea. Non vale il rischio, o lo sforzo richiesto a lungo termine.

    
risposta data 30.10.2014 - 02:50
fonte

Leggi altre domande sui tag