vector :: at vs. vector :: operator []


95

So che at()è più lento che a []causa del controllo dei confini, che è anche discusso in domande simili come C ++ Vector at / [] operator speed o :: std :: vector :: at () vs operator [] << risultati sorprendenti !! Da 5 a 10 volte più lento / veloce! . Semplicemente non capisco a cosa at()serve il metodo.

Se ho un vettore semplice come questo: std::vector<int> v(10);e decido di accedere ai suoi elementi usando at()invece che []in situazioni in cui ho un indice ie non sono sicuro che sia nei limiti dei vettori, mi costringe a avvolgerlo con try-catch blocco :

try
{
    v.at(i) = 2;
}
catch (std::out_of_range& oor)
{
    ...
}

anche se sono in grado di ottenere lo stesso comportamento utilizzando size()e controllando l'indice da solo, il che sembra più facile e molto conveniente per me:

if (i < v.size())
    v[i] = 2;

Quindi la mia domanda è:
quali sono i vantaggi dell'utilizzo di vector :: at over vector :: operator [] ?
Quando dovrei usare vector :: at anziché vector :: size + vector :: operator [] ?


11
+1 domanda molto buona !! ma non credo che a () sia quello comunemente usato.
Rohit Vipin Mathews,

10
Nota che nel tuo codice di esempio if (i < v.size()) v[i] = 2;, c'è un possibile percorso di codice che non assegna affatto 2a nessun elemento di v. Se questo è il comportamento corretto, bene. Ma spesso non c'è niente di sensato che questa funzione possa fare quando i >= v.size(). Quindi non c'è motivo particolare per cui non dovrebbe utilizzare un'eccezione per indicare una situazione imprevista. Molte funzioni usano semplicemente operator[]senza controllare la dimensione, il documento che ideve essere nell'intervallo e incolpano l'UB risultante sul chiamante.
Steve Jessop

Risposte:


74

Direi che le eccezioni che vector::at()genera non sono realmente destinate ad essere catturate dal codice immediatamente circostante. Sono utili principalmente per rilevare bug nel codice. Se è necessario controllare i limiti in fase di esecuzione perché ad esempio l'indice proviene dall'input dell'utente, è davvero meglio con ifun'istruzione. Quindi, in sintesi, progetta il tuo codice con l'intenzione che vector::at()non genererà mai un'eccezione, in modo che se lo fa e il tuo programma si interrompe, è un segno di un bug. (proprio come un assert())


1
+1 Mi piace la spiegazione di come separare la gestione dell'input dell'utente sbagliato (convalida dell'input; ci si potrebbe aspettare un input non valido, quindi non è considerato qualcosa di eccezionale) ... e bug nel codice (dereferenziare l'iteratore fuori intervallo è eccezionale cosa)
Bojan Komazec

Quindi dici che dovrei usare size()+ []quando index dipende dall'input dell'utente, usalo assertin situazioni in cui index non dovrebbe mai essere fuori dai limiti per una facile correzione di bug in futuro e .at()in tutte le altre situazioni (nel caso, potrebbe accadere qualcosa di sbagliato .. .)
LihO

8
@LihO: se la tua implementazione offre un'implementazione di debug vector, probabilmente è meglio usarla come opzione "just in case" piuttosto che at()ovunque. In questo modo puoi sperare in un po 'più di prestazioni in modalità di rilascio, nel caso in cui ne avessi bisogno.
Steve Jessop

3
Sì, la maggior parte delle implementazioni STL in questi giorni supporta una modalità di debug che controlla anche i limiti operator[], ad esempio gcc.gnu.org/onlinedocs/libstdc++/manual/… quindi se la tua piattaforma lo supporta, probabilmente farai meglio a farlo!
pmdj

1
@pmdj punto fantastico, di cui non sapevo ... ma link orfano. : P quello attuale è: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
underscore_d

16

mi costringe a avvolgerlo con il blocco try-catch

No, non è così (il blocco try / catch può essere a monte). È utile quando si desidera che venga lanciata un'eccezione piuttosto che il programma per entrare nel regno del comportamento indefinito.

Sono d'accordo che la maggior parte degli accessi fuori limite ai vettori sono un errore del programmatore (nel qual caso dovresti usarli assertper individuare quegli errori più facilmente; la maggior parte delle versioni di debug delle librerie standard lo fa automaticamente per te). Non vuoi usare eccezioni che possono essere inghiottite a monte per segnalare errori del programmatore: vuoi essere in grado di correggere il bug .

Poiché è improbabile che un accesso fuori limite a un vettore faccia parte del normale flusso del programma (nel caso lo sia, hai ragione: controlla in anticipo sizeinvece di lasciare che l'eccezione si manifesti), sono d'accordo con la tua diagnostica: atè essenzialmente inutile.


Se non rilevo l' out_of_rangeeccezione, abort()viene chiamato.
LihO

@ LihO: non necessariamente..la try..catchpuò essere presente nel metodo che chiama questo metodo.
Naveen

12
Se non altro, atè utile nella misura in cui altrimenti ti ritroveresti a scrivere qualcosa di simile if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }. Le persone spesso pensano alle funzioni che generano eccezioni in termini di "maledizioni, devo gestire l'eccezione", ma fintanto che documenti attentamente ciò che ciascuna delle tue funzioni può lanciare, possono anche essere usate come "fantastico, non lo faccio devo controllare una condizione e lanciare un'eccezione ".
Steve Jessop

@SteveJessop: non mi piace lanciare eccezioni per bug di programma, poiché possono essere rilevati a monte da altri programmatori. Le asserzioni sono molto più utili qui.
Alexandre C.

6
@AlexandreC. beh, la risposta ufficiale a questa domanda è che out_of_rangederiva da logic_error, e altri programmatori "dovrebbero" saperlo meglio piuttosto che prenderli a logic_errormonte e ignorarli. assertpuò essere ignorato anche se i tuoi colleghi vogliono non sapere dei loro errori, è solo più difficile perché devono compilare il tuo codice con NDEBUG;-) Ogni meccanismo ha i suoi pregi e difetti.
Steve Jessop,

11

Quali sono i vantaggi nell'usare vector :: at rispetto a vector :: operator []? Quando dovrei usare vector :: at anziché vector :: size + vector :: operator []?

Il punto importante qui è che le eccezioni consentono la separazione del normale flusso di codice dalla logica di gestione degli errori e un singolo blocco catch può gestire i problemi generati da una miriade di siti di lancio, anche se disseminati in profondità all'interno delle chiamate di funzione. Quindi, non at()è necessariamente più facile per un singolo utilizzo, ma a volte diventa più facile - e meno offuscamento della logica del caso normale - quando hai molte indicizzazioni da convalidare.

È anche degno di nota il fatto che in alcuni tipi di codice un indice viene incrementato in modi complessi e utilizzato continuamente per cercare un array. In questi casi, è molto più facile garantire controlli corretti utilizzando at().

Come esempio del mondo reale, ho codice che tokenizza C ++ in elementi lessicali, quindi altro codice che sposta un indice sul vettore di token. A seconda di ciò che si incontra, potrei voler incrementare e controllare l'elemento successivo, come in:

if (token.at(i) == Token::Keyword_Enum)
{
    ASSERT_EQ(tokens.at(++i), Token::Idn);
    if (tokens.at(++i) == Left_Brace)
        ...
    or whatever

In questo tipo di situazione, è molto difficile controllare se hai raggiunto in modo inappropriato la fine dell'input perché dipende molto dagli esatti token incontrati. Il controllo esplicito in ogni punto di utilizzo è doloroso e c'è molto più spazio per gli errori del programmatore poiché aumenti pre / post, offset al punto di utilizzo, ragionamento errato sulla validità continua di alcuni test precedenti ecc.


10

at può essere più chiaro se hai un puntatore al vettore:

return pVector->at(n);
return (*pVector)[n];
return pVector->operator[](n);

Prestazioni a parte, il primo di questi è il codice più semplice e chiaro.


... specialmente quando hai bisogno di un puntatore all'n -esimo elemento di un vettore.
delfino

4

Innanzitutto, non viene specificato se at()o operator[]è più lento. Quando non ci sono errori di limite, mi aspetto che abbiano all'incirca la stessa velocità, almeno nel debug delle build. La differenza è che at()specifica esattamente cosa accadrà in presenza di un errore di limite (un'eccezione), dove come nel caso di operator[], è un comportamento indefinito: un arresto anomalo in tutti i sistemi che uso (g ++ e VC ++), almeno quando vengono utilizzati i normali flag di debug. (Un'altra differenza è che una volta che sono sicuro del mio codice, posso ottenere un aumento sostanziale della velocità operator[] disattivando il debug. Se le prestazioni lo richiedono, non lo farei a meno che non fosse necessario.)

In pratica, at()è raramente appropriato. Se il contesto è tale che sai che l'indice potrebbe non essere valido, probabilmente vuoi il test esplicito (ad esempio per restituire un valore predefinito o qualcosa del genere), e se sai che non può essere non valido, vuoi interrompere (e se non sai se può essere non valido o meno, ti suggerisco di specificare più precisamente l'interfaccia della tua funzione). Esistono alcune eccezioni, tuttavia, in cui l'indice non valido può derivare dall'analisi dei dati dell'utente e l'errore dovrebbe causare l'interruzione dell'intera richiesta (ma non arrestare il server); in questi casi, un'eccezione è appropriata e at()lo farà per te.


4
Perché ti aspetteresti che abbiano all'incirca la stessa velocità, quando operator[]non è costretto a fare il controllo dei limiti, mentre lo at()è? Ciò implica problemi di cache, speculazione e branching buffer?
Sebastian Mach

@phresnel operator[]non è tenuto a fare il controllo dei limiti, ma tutte le buone implementazioni sì . Almeno in modalità di debug. L'unica differenza è cosa fanno se l'indice è fuori limite: si operator[]interrompe con un messaggio di errore, at()genera un'eccezione.
James Kanze

2
Spiacenti, manca il tuo attributo "in modalità debug". Tuttavia, non misurerei il codice sulla sua qualità in modalità di debug. In modalità di rilascio, il controllo è richiesto solo da at().
Sebastian Mach

1
@phresnel La maggior parte del codice che ho consegnato era in modalità "debug". Si disattiva il controllo solo quando i problemi di prestazioni lo richiedono effettivamente. (Microsoft pre-2010 era un po 'un problema qui, poiché std::stringnon sempre funzionava se le opzioni di controllo non corrispondevano a quelle del runtime:, -MDe faresti meglio a disattivare il controllo -MDd, e faresti meglio a su.)
James Kanze

2
Sono più del campo che dice "codice come sancito (garantito) dallo standard"; ovviamente sei libero di fornire in modalità di debug, ma quando esegui lo sviluppo multipiattaforma (incluso, ma non esclusivamente, il caso dello stesso sistema operativo, ma diverse versioni del compilatore), fare affidamento sullo standard è la soluzione migliore per i rilasci e la modalità di debug è considerato uno strumento per il programmatore per ottenere quella cosa per lo più corretta e robusta :)
Sebastian Mach

1

Il punto centrale dell'utilizzo delle eccezioni è che il codice di gestione degli errori può essere più lontano.

In questo caso specifico, l'input dell'utente è davvero un buon esempio. Immagina di voler analizzare semanticamente una struttura dati XML che utilizza indici per fare riferimento a un tipo di risorsa che memorizzi internamente in un file std::vector. Ora l'albero XML è un albero, quindi probabilmente vorrai usare la ricorsione per analizzarlo. In fondo, nella ricorsione, potrebbe esserci una violazione di accesso da parte del writer del file XML. In tal caso, di solito vuoi superare tutti i livelli di ricorsione e rifiutare semplicemente l'intero file (o qualsiasi tipo di struttura "più grossolana"). È qui che torna utile. Puoi semplicemente scrivere il codice di analisi come se il file fosse valido. Il codice della libreria si occuperà del rilevamento degli errori e tu puoi semplicemente catturare l'errore a livello grossolano.

Inoltre, altri contenitori, come std::map, hanno anche una std::map::atsemantica leggermente diversa da std::map::operator[]: at può essere utilizzato su una mappa const, mentre operator[]non può. Ora, se volessi scrivere codice agnostico del contenitore, come qualcosa che potrebbe occuparsi di const std::vector<T>&o const std::map<std::size_t, T>&, ContainerType::atsarebbe la tua arma preferita.

Tuttavia, tutti questi casi di solito si verificano quando si gestisce una sorta di input di dati non convalidato. Se sei sicuro del tuo intervallo valido, come normalmente dovresti essere, di solito puoi usare operator[], ma meglio ancora, iteratori con begin()e end().


1

Secondo questo articolo, prestazioni a parte, non fa alcuna differenza da utilizzare ato operator[], solo se l'accesso è garantito entro le dimensioni del vettore. Altrimenti, se l'accesso è basato solo sulla capacità del vettore, è più sicuro da usare at.


1
là fuori ci sono draghi. cosa succede se clicchiamo su quel link? (suggerimento: lo so già, ma su StackOverflow preferiamo commenti che non soffrano di link rot, ovvero forniamo un breve riassunto su ciò che vuoi dire)
Sebastian Mach

Grazie per il consiglio. Adesso è stato risolto.
ahj

0

Nota: sembra che alcune nuove persone stiano votando negativamente questa risposta senza avere la cortesia di dire cosa non va. La risposta di seguito è corretta e può essere verificata qui .

C'è davvero solo una differenza: atcontrolla i limiti mentre operator[]no. Questo vale sia per le build di debug che per le build di rilascio e questo è molto ben specificato dagli standard. È così semplice.

Questo rende atun metodo più lento, ma è anche un pessimo consiglio da non usare at. Devi guardare i numeri assoluti, non i numeri relativi. Posso tranquillamente scommettere che la maggior parte del tuo codice sta facendo operazioni più costose di at. Personalmente, cerco di usare atperché non voglio che un brutto bug crei comportamenti indefiniti e si intrufoli nella produzione.


1
Le eccezioni in C ++ sono pensate per essere un meccanismo di gestione degli errori, non uno strumento per il debug. Herb Sutter spiega perché il lancio std::out_of_rangeo qualsiasi altra forma std::logic_errorè, in effetti, un errore logico di per sé qui .
Big Temp,

@ BigTemp - Non sono sicuro di come il tuo commento si riferisca a questa domanda e risposta. Sì, le eccezioni sono un argomento molto dibattuto, ma la domanda qui è la differenza tra ate []e la mia risposta afferma semplicemente la differenza. Io personalmente uso il metodo "sicuro" quando la perfom non è un problema. Come dice Knuth, non fare un'ottimizzazione prematura. Inoltre, è bene che i bug vengano rilevati presto rispetto alla produzione indipendentemente dalle differenze filosofiche.
Shital Shah
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.